Java中递归方法的学习和应用
1.递归算法
递归在计算机科学中也被称为递归算法,一些程序问题在分解时,会发现这样一种现象:解决该问题的过程是由重复的相同过程组成。类似这样的问题我们都可以通过递归算法进行解决。在计算机语言中,递归算法的实现靠函数的自我调用来完成,我们所能见到的大多数高级编程语言都支持这样的做法。
递归算法通常有两种方式进行——普通递归和尾递归。递归方法简单的来说就是方法的内部再次调用自身,递归方法会嵌套式的参与运算。这样每一个递归方法都要分配一个函数堆栈进行操作,这就是普通递归,它对内存的消耗是非常大的。
此外,还有一种递归方式被称为尾递归,尾递归对普通递归进行了优化。如果使用尾递归,需要将递归方式进行特殊的设计,它需要将递归方法在return语句后进行单独调用(即尾调用)。当采用尾递归的时候,会在同一个函数堆栈中进行,效率非常快。作为一名Java程序员,如果你无法将递归方法设计成尾递归的模式也没有任何问题,因为Java并没有对尾递归进行优化,Java对内存的优化是依赖于垃圾收回器。但是如果你是一名C程序员,就需要对尾递归的写法进行掌握了。
递归算法的优缺点是非常明显,算法实现简单、可读性强是递归算法的优点所在。缺点也同样明显,递归算法会占用大量内存空间,如果递归深度过大,容易发生内存问题。
2.递归的使用
在设计递归算法的时候,一定要注意两点:1、设计出等价的递归公式。这一点需要我们拥有一些数学基础以及抽象概括能力,等够在复杂的运行过程中,抽象出等价的函数关系。2、递归结束的条件。这一点尤为重要,如果递归方法没有结束条件,就如同死循环一样,让内存和CPU直接"撑爆"。
2.1 斐波拉切数列(Fibonacci sequence)
在编程语言的学习中,解决一些数学常见问题是学习编程的一种途径。斐波拉切数列就经常出现在编程语言练习中,它是一组有规律数列:“1,1,2,3,5,8,13,21……”,当我们要获取数列中第n位的数字时,可以总结如下公式:
当n=1或者2时,有f(n)=1,当>=3时,有f(n)=f(n-1)+f(n-2)
当我们要设计一个方法,输出数列的前n个数字的信息,n通过整型参数控制。如果我们需要一个完整的数列,就需要创建一个数列容器,然后将数列中的每一位数字依次计算出来,并保存到容器中,最后按照顺序从容器中输出数列(如下列Java示例):
01. public static int[] fibs(int n) {
02. if(n<=0) return new int[0];
03. int[] fib=new int[n];
04.
05. for(int i=0;i<n;i++) {
06. if(i==0 || i==1) {
07. fib[i]=1;
08. }else {
09. fib[i]=fib[i-1]+fib[i-2];
10. }
11. }
12. return fib;
13. }
采用上面的做法好处非常明显,它能够记录每一位数列的值。当我们需要获取整个数列的时候,这样的方式是可取的。在一些时候,我们只想获取其中一位的数值,我们就不需要记录数列,这个时候使用递归的方式就非常方便(如下Java示例所示):
01. public static int fib(int n) {
02. if(n<=0) return 0;
03. if(n==1 || n==2) return 1;
04.
05. return fib(n-1)+fib(n-2);
06. }
采用上述代码,可以直接获取到数列中第n位的数值,通过观察代码可以发现,使用递归的方式让代码更简洁、阅读起来更方便。我们创建两个测试方法,对两种数列的获取进行测试:
01. //打印数列
02. public static void show(int n) {
03. int[] array=fibs(n);
04. System.out.println("Fibs (n="+n+")"+Arrays.toString(array));
05. }
06. //打印数列中指定位置的数值
07. public static void showBit(int n) {
08. System.out.println("Fib "+n+" bit="+fib(n));
09. }
10.
11. public static void main(String[] args) {
12. Fibonacci.show(1);
13. Fibonacci.show(2);
14. Fibonacci.show(3);
15. Fibonacci.show(10);
16. Fibonacci.showBit(1);
17. Fibonacci.showBit(2);
18. Fibonacci.showBit(3);
19. Fibonacci.showBit(5);
20. }
运行结果:
Fibs (n=1):[1]
Fibs (n=2):[1, 1]
Fibs (n=3):[1, 1, 2]
Fibs (n=10):[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Fib 1 bit=1
Fib 2 bit=1
Fib 3 bit=2
Fib 5 bit=5
2.2 汉诺塔(Hanoi)
汉诺塔是一种有趣的益智游戏,很多人在儿时都玩过这种类似的玩具(如下图所示):
汉诺塔的玩法规则是将所有圆盘从A柱上移动到C柱上,并保持从上到下按照大小顺序排放。在移动的过程中,也需要保持从上到下的大小规则。例如上图的三层安诺塔,我们在移动的时候有如下步骤(如下图所示):
如果有多个盘子,我们设盘子总数为n,我们可以分为两部分解决,一部分是上面的n-1个盘子,它们
作为一个整体,另一部分是最下面的的盘子n。它们移动可以分为三步:
1. 将第一部分的n-1个盘子的作为一个整体,从A移动到B柱上,C柱做过度柱。
2. 接着将第n个盘子从A柱移动到C柱上。
3. 再将n-1个盘子的整体从B柱移动到C柱上,A柱做过度柱。
在移动的过程中,需要用剩余的柱子做过度柱。移动过程如下图所示:
在用代码实现的时候,我们就可以利用递归的方式进行移动。在下面的代码中,我们为了能够追溯每一步移动时,各个柱子上的盘子情况,我们用一个队列来模拟柱子(实现代码如下所示):
01. import java.util.HashMap;
02. import java.util.LinkedList;
03. import java.util.Map;
04.
05. public class Hanoi {
06. //柱子
07. private Map<String,LinkedList<Integer>> pillar;
08. private LinkedList<Integer> a=new LinkedList<>();//a柱
09. private int step=0;
10. public Hanoi(int number) {
11. //初始化A柱
12. for(int i=1;i<=number;i++) {
13. a.add(i);
14. }
15.
16. pillar=new HashMap<>();
17. pillar.put("A", a);
18. pillar.put("B", new LinkedList<Integer>());
19. pillar.put("C", new LinkedList<Integer>());
20. }
21.
22. public void play() {
23. move(a.size(),"A","B","C");
24. }
25. //a是移出住,b是过度柱,c是目标柱.
26. //通过a,b,c三个柱子名称,可以从pillar中获取到对应柱子的盘子队列
27. public void move(int number,String a,String b,String c) {
28. if(number==1) {//第一个盘子,直接移动到目标柱上
29. System.out.print("移动圆盘"+number+"从"+a+"柱到"+c+"柱上");
30. pillar.get(c).push(pillar.get(a).pop());//移动柱子中的圆盘
31. addStep();
32. }else {
33. move(number-1,a,c,b);//从a移动到b,c做过度step1
34. System.out.print("移动了圆盘"+number+"从"+a+"柱到了"+c+"柱上");
35. pillar.get(c).push(pillar.get(a).pop());//移动柱子中的圆盘step2
36. addStep();
37. move(number-1,b,a,c);//从b移动到c,a做过度,step3
38. }
39. }
40.
41. public void addStep() {
42. this.step++;
43. System.out.println(",共移动了"+step+"步");
44. System.out.println("A柱(上到下)"+":"+pillar.get("A"));
45. System.out.println("B柱(上到下)"+":"+pillar.get("B"));
46. System.out.println("C柱(上到下)"+":"+pillar.get("C"));
47. }
48.
49. public static void main(String[] args) {
50. Hanoi han=new Hanoi(3); //测试3个圆盘的移动
51. han.play();
52. }
53. }
示例运行效果:
移动圆盘1从A柱到C柱上,共移动了1步
A柱(上到下):[2, 3]
B柱(上到下):[]
C柱(上到下):[1]
移动了圆盘2从A柱到了B柱上,共移动了2步
A柱(上到下):[3]
B柱(上到下):[2]
C柱(上到下):[1]
移动圆盘1从C柱到B柱上,共移动了3步
A柱(上到下):[3]
B柱(上到下):[1, 2]
C柱(上到下):[]
移动了圆盘3从A柱到了C柱上,共移动了4步
A柱(上到下):[]
B柱(上到下):[1, 2]
C柱(上到下):[3]
移动圆盘1从B柱到A柱上,共移动了5步
A柱(上到下):[1]
B柱(上到下):[2]
C柱(上到下):[3]
移动了圆盘2从B柱到了C柱上,共移动了6步
A柱(上到下):[1]
B柱(上到下):[]
C柱(上到下):[2, 3]
移动圆盘1从A柱到C柱上,共移动了7步
A柱(上到下):[]
B柱(上到下):[]
C柱(上到下):[1, 2, 3]
3.递归对循环的替代
在程序开发过程中,很多循环方法都可以使用递归来完成,例如数字的累加和阶层的计算。在下面的代码示例中我们对比这两种算法:
01. public class Compute {
02.
03. //使用循环完成累加
04. public static int sum(int start,int end) {
05. int sum=0;
06. for(int i=start;i<=end;i++) {
07. sum+=i;
08. }
09.
10. return sum;
11. }
12. //使用递归进行累加计算
13. public static int recSum(int start,int end) {
14.
15. if(end<start) return 0;
16.
17. return end==start?start:end+recSum(start,end-1);
18. }
19.
20. public static int factorial(int number) {
21. if(number<=0) return 0;
22. int result=1;
23. for(int i=1;i<=number;i++) {
24. result*=i;
25. }
26.
27. return result;
28. }
29.
30. public static int recFactorial(int number) {
31. if(number<=0) return 0;
32. return number==1?1:number*recFactorial(number-1);
33. }
34.
35. public static void main(String[] args) {
36. System.out.println("非递归累加0~100="+Compute.sum(0, 100));
37. System.out.println("递归累加0~100="+Compute.recSum(0,100));
38. System.out.println("非递归阶乘6="+Compute.factorial(6));
39. System.out.println("递归阶乘6="+Compute.recFactorial(6));
40. }
41. }
运行效果:
非递归累加0~100=5050
递归累加0~100=5050
非递归阶乘6=720
递归阶乘6=720
在上述示例代码中,我们用递归和非递归两种方式解决了累加和循环问题。除此之外,在一些数据结构算法中,递归的使用也非常多,比如二叉树的遍历、排序等,在下面的示例中,我们使用递归的方法进行冒泡排序,并与传统的冒泡排序进行对比:
01. import java.util.Arrays;
02.
03. public class Sort {
04.
05. //冒泡排序
06. public static void bubbleSort(int[] array) {
07. for(int i=0;i<array.length;i++) {//外层循环,记录排序的轮次
08. /** 内存循环用于俩俩比较,从数组第一个元素比较到数组长度减i个轮次.
09. * 第一轮从1到length-0,第二轮从1到length-1,依次类推*/
10. for(int j=1;j<array.length-i;j++) {
11. //当前一元素大于后一个元素,交换二者的位置。实现大元素后移
12. if(array[j-1]>array[j]) {
13. int temp=array[j-1];
14. array[j-1]=array[j];
15. array[j]=temp;
16. }
17. }
18. }
19. }
20.
21. //冒泡排序递归
22. public static void recBubbleSort(int[] array,int index) {
23.
24. for(int i=index+1;i<array.length;i++) {
25. if(array[index]>array[i]) {
26. int temp=array[index];
27. array[index]=array[i];
28. array[i]=temp;
29. }
30. }
31. if(index<array.length) recBubbleSort(array, index+1);
32. }
33.
34. public static void main(String[] args) {
35. int[] array= {1,4,3,-100,323,53,13,554,123,20,43,12};
36. //Sort.bubbleSort(array);
37. Sort.recBubbleSort(array,0);
38. System.out.println(Arrays.toString(array));
39. }
40. }
传统的冒泡排序需要借助于双层循环进行排序交换,利用递归的方式,可以减少一层循环。在实际的排序中,我们是不推荐使用递归进行排序的,上述示例仅作为递归算法的一种思考。