数据结构和算法学习(6)-递归

所谓递归指的是函数自己调用自己的一种编程方法

递归可以完成复杂的大量的重复性工作,并由一定条件控制递归的返回

也就是说,递归的使用需要满足1.可以划分为多个具有相同操作的子分支的问题 2.递归不可以无限制调用,需要有条件进行控制

经典问题

三角数字

形如1,3,6,10,15,21....之类的数列中所包含的数字被称为三角数字

                              *

                  *         **

         *      **       ***    ……

*     **     ***     ****

1      3       6         10      ……

分析

它的规律是第n项数字是由第n-1项数字加上n得到的。所以要取得第n项就要先知道第n-1项,要知道第n-1项就必须要知道第n-2项,依此类推

这是一个循环,然而在第1项数字是可以确定为1,也就是有了控制循环的条件,所以这个问题的解法可以用递归完成

示例代码

import java.util.Scanner;

public class Test {

	public static void main(String[] args) {
		Scanner sc=new Scanner(System.in);
		int n=0;
		for(;;){
			n=sc.nextInt();
			System.out.println(triangleNum(n));
		}
	}
	
	public static int triangleNum(int n){
		if (n==1) {
			return 1;
		}else {
			return triangleNum(n-1)+n;
		}
	}
}

变位字

排列是指按照一定的顺序安排事务,列出一个指定单词的所有变位字,也就是列出该词的全排列(无论这些排列是否都是真正的英语单词)

它们都是由这个单词的字母组成,不同之处在于字母的顺序不同。

全排列cat会产生:cat cta atc act tca tac

分析

假设需要全排列的单词有n个字母,首先全排列从右边起的n-1个字母,然后轮换所有n个字母,然后重复此步骤n次。

轮换意味着所有字母都向左移一位,最左边的字母换至最右边。

如下图:


示例代码

import java.util.Scanner;

public class Test {

	static char[] schs=null;
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		String word = sc.next();
		sc.close();
		rotate(word);
	}

	public static void rotate(String word) {
		char[] chs = word.toCharArray();
		int length = word.length();
		int position = 0;
		if (length <= 1) {
			return;
		}
		for (int i = 0; i < chs.length; i++) {
			char ch = chs[position];
			for (int j = 0; j < chs.length-1; j++) {
				chs[j + position] = chs[j + position + 1];
			}
			chs[position + length - 1] = ch;
			schs=null;
			schs=new char[chs.length];
			for (int j = 0; j < chs.length; j++) {
				schs[j]=chs[j];
			}
			rank(position, length, schs);
		}
	}

	public static void rank(int position, int length, char[] chs) {
		char ch = chs[position];
		for (int j = 0; j < length - 1; j++) {
			chs[j + position] = chs[j + position + 1];
		}
		chs[position + length - 1] = ch;
		for (char c : chs) {
			System.out.print(c);
		}
		System.out.println();
		position++;
		length--;
		if (length > 1) {
			rank(position, length, chs);
		}
	}
}

诚实说笔者自己写的这一串代码还不够完善,究其原因还是因为虽然对递归思想的整体把握虽然已经形成,但是部分实现所用的代码还是考虑不周

但是如上所示的代码是完全正确的,只是易读性不够好,有兴趣的读者可以自行完善

递归的二分查找

在学习时间复杂度是讨论过二分查找,是一种效率不错的查找方法,当时是采用循环的方法

代码如下

import java.util.Random;
import java.util.Scanner;

public class Test {

	public static void main(String[] args) {
		int[] array =new int[100];
		Random r=new Random();
		for (int i = 0; i < array.length; i++) {
			array[i]=(r.nextInt(2)+i*3);
			System.out.print(array[i]+" ");
			if ((i+1)%10==0) {
				System.out.println();
			}
		}
		Scanner sc=new Scanner(System.in);
		int key =sc.nextInt();
		sc.close();
		System.out.println(find(key,array));
	}

	public static int find(int key,int[] array) {
		int lowerBound=0;
		int upperBound=array.length-1;
		int curInt;
		for(;;){
			curInt=(lowerBound+upperBound)/2;
			if (array[curInt]==key) {
				return curInt;
			}
			else if (lowerBound>upperBound) {
				return -1;
			}
			else {
				if (array[curInt]>key) 
					upperBound=curInt-1;
				else 
					lowerBound=curInt+1;
			}
		}
	}

}

我们可以很容易的吧基于循环的方法转换成基于递归的方法

代码如下

import java.util.Random;
import java.util.Scanner;

public class Test {

	static int[] arrays;

	public static void main(String[] args) {
		arrays = new int[100];
		Random r = new Random();
		for (int i = 0; i < arrays.length; i++) {
			arrays[i] = (r.nextInt(2) + i * 3);
			System.out.print(arrays[i] + " ");
			if ((i + 1) % 10 == 0) {
				System.out.println();
			}
		}
		Scanner sc = new Scanner(System.in);
		int key = sc.nextInt();
		sc.close();
		int lowerBound = 0;
		int upperBound = arrays.length - 1;
		System.out.println(find(key, upperBound, lowerBound));
	}

	public static int find(int key, int upperBound, int lowerBound) {
		int curInt = (lowerBound + upperBound) / 2;
		if (arrays[curInt] == key) {
			return curInt;
		} else if (lowerBound > upperBound) {
			return -1;
		} else {
			if (arrays[curInt] > key)
				return find(key, curInt - 1, lowerBound);
			else
				return find(key, upperBound, curInt + 1);
		}
	}
}

汉诺塔问题

汉诺塔问题不再赘述,不了解的请自行百度

主要思路:

假设有N个盘子要从A移动至C,则先考虑将N-1个盘子移动至B,即先将N-2个盘子移动至C

依此类推实现递归调用

代码如下

public class Test {


	public static void main(String[] args) {
		int topFloor = 3;
		char from='A';
		char transit='B';
		char to='C';
		move(topFloor, from, transit, to);
	}

	public static void move(int topFloor, char from, char transit,char to) {
		if (topFloor==1) {
			System.out.println("Disk 1 from "+ from +" to "+to);
		}
		else {
			move(topFloor-1, from, to, transit);
			System.out.println("Disk "+topFloor+" from "+ from +" to "+to);
			move(topFloor-1, transit, from, to);
		}
	}
}

归并排序

归并排序的中心思想是归并两个已经有序的数组生成新数组,新数组包括原有的两个有序数组的所有数据。

复杂度

O(N*logN)

缺点

归并排序需要在存储器中新建一个大小等同于需要被排序的两个数组的和的数组,如果没有足够的空间,归并排序将不能运行

代码实现

在这里对于采用循环的方法不再进行练习,仅仅讨论递归的方法

归并排序对于单个数组的操作是讲一个数组分成两半,单独对每一半进行排序,然后再进行归并排序,而单独为每一半排序的时候又可以将这一半再拆成大小相同的两半依此类推

代码如下

import java.util.Arrays;
import java.util.Random;

public class Test {

	public static void main(String[] args) {
		int[] array = new int[10000];
		Random r = new Random();
		for (int i = 0; i < array.length; i++) {
			array[i] = r.nextInt(100);
			System.out.print(array[i] + " ");
			if ((i + 1) % 10 == 0) {
				System.out.println();
			}
		}
		int[] sortArray = divideArray(array);
		for (int i = 0; i < sortArray.length; i++) {
			System.out.print(sortArray[i] + " ");
			if ((i + 1) % 20 == 0) {
				System.out.println();
			}
		}
	}

	public static int[] divideArray(int[] needSortArray) {
		if (needSortArray.length <= 1) {
			return needSortArray;
		}
		int lowerBound = 0;
		int upperBound = needSortArray.length - 1;
		if (needSortArray.length % 2 == 0) {
			int[] arrayFirst = Arrays.copyOfRange(needSortArray, lowerBound,
					(lowerBound + upperBound) / 2 + 1);
			int[] arraySecond = Arrays.copyOfRange(needSortArray,
					(lowerBound + upperBound) / 2 + 1, upperBound + 1);
			return mergeSort(divideArray(arrayFirst), divideArray(arraySecond));
		} else {
			int[] arrayFirst = Arrays.copyOfRange(needSortArray, lowerBound,
					(lowerBound + upperBound) / 2);
			int[] arraySecond = Arrays.copyOfRange(needSortArray,
					(lowerBound + upperBound) / 2, upperBound + 1);
			return mergeSort(divideArray(arrayFirst), divideArray(arraySecond));
		}
	}

	public static int[] mergeSort(int[] arrayFirst, int[] arraySecond) {
		int[] sortArray = new int[arrayFirst.length + arraySecond.length];
		if (arrayFirst.length == 1 && arraySecond.length == 1) {
			if (arrayFirst[0] < arraySecond[0]) {
				sortArray[0] = arrayFirst[0];
				sortArray[1] = arraySecond[0];
			} else {
				sortArray[1] = arrayFirst[0];
				sortArray[0] = arraySecond[0];
			}
			return sortArray;
		}
		int af = 0;
		int as = 0;
		for (int i = 0; i < sortArray.length; i++) {
			if (af>=arrayFirst.length) {
				sortArray[i] = arraySecond[as];
				as++;
			}else if (as>=arraySecond.length) {
				sortArray[i] = arrayFirst[af];
				af++;
			}
			else if (arrayFirst[af] >= arraySecond[as]) {
				sortArray[i] = arraySecond[as];
				as++;
			} else {
				sortArray[i] = arrayFirst[af];
				af++;
			}
		}
		return sortArray;
	}
}
代码自己在理解的基础上手写,并未参照任何代码,着重体现递归思想,在归并部分的判断可能有些考虑不周,仅供参考

归并排序的复制和比较

如图


消除递归

正如大家所看到的,有一些算法趋于使用递归的方法,而另一些则不是。有一些递归的方法也可以用简单的循环来实现,来使得效率更好。但是,各种分治算法,如归并排序的递归函数能比循环更好的工作

使用递归的算法从概念上更容易理解,但是一些实际运用中递归算法的效率并不是太高,所以在这种情况下,把递归转换成非递归是非常有用的。而这通常会用到栈

递归和栈

递归和栈之间有着紧密的联系,而且大部分的编译器都是使用栈来实现递归的。

当调用一个方法是,编译器会把这个方法所有的参数和返回地址都压入栈中,然后把控制转移给这个方法。当这个方法返回的时候,这些值退栈,并且控制权重新回到返回地址处

public int rank(int n){
        if (n==1) {
            return n;
        }else {
            return n+rank(n-1);
        }
    }
上面所示的是一个简单的递归例子

这个算法会被分解成一个个单独的操作,是每一个操作对应switch中的一条case语句。switch被封装在一个名为step()的方法中,每次调用step()都会执行switch中的一个case。反复调用step()将会执行所有代码

这个rank()方法执行了两种操作。第一种是他执行必要的算术运算来进行计算并检查n是否为1以及给上一次递归调用的结果加n。另一种就是执行了管理方法本身的一些必要操作,包括控制转移、参数的读取以及返回地址。简单来讲就是如下几步:

1.当一个方法被调用时,他的参数以及返回地址被压入一个栈中

2.这个方法可以通过获取栈顶元素的值来访问他的参数

3.当这个方法要返回的时候,他查看栈疑惑的返回地址,然后这个地址以及方法所有的参数退栈,并且销毁




















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值