【Java方法笔记】3-5 递归

§3-5 递归

递归是一种常见的算法思想,在程序设计中被广泛运用。本节将简单地介绍递归。

3-5.1 何谓递归?

程序调用自身的编程技巧称为递归(recursion)。利用递归可以用简单的程序解决一些复杂的问题。它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需要少量的程序就描述出解题过程所需的多次重复计算,大大减少了程序的代码量。

递归结构包括两个部分

  • 递归头:何时不调用自身方法。若此部分丢失,将陷入死循环。

    Exception in thread "main" java.lang.StackOverflowError:栈溢出。

  • 递归体:何时需要调用自身方法。

采用递归思想的几个条件

  • 该模块自身具备递归算法或性质(例如自身定义自身,如阶乘、自然数的定义等)
  • 随着递归迭代次数的增多问题规模逐渐减小
  • 迭代存在边界条件,不会无限递归

但在使用递归时,我们得要清楚

3 - 1 方法:定义与调用中提到, Java 使用的是栈机制,递归运行实际上就是多个方法的嵌套调用,方法调用会进栈占用内存,递归次数越多,占用内存越大,main方法结束时,弹栈消失,内存腾出。因此,当计算量不大时,递归思想可以考虑使用。并且,递归算法是深度优先,即直至调用的最里层方法(最后入栈的方法)执行完毕后,才会向其被调用方法返回值。

从这一点上看,递归算法的缺点就十分明显:效率低、占用高

3-5.2 递归的应用

接下来我们将用几个例子介绍递归。

3-5.2.1 阶乘运算

public class Recursion {
    public static void main(String[] args) {
        //阶乘
        Recursion recursion = new Recursion();
        System.out.println(recursion.factorial(5));
    }
    // n ! = n * (n-1) * (n-2) * ... * 1
    public int factorial(int n) {
        if ( n == 1) {
            return 1;
        } else if ( n <= 0) {
            return 1;
        } else {
            return n * factorial(n-1);
        }
    }
}

输出 5 的阶乘:

image

在上述例子中,n=1是递归头,当n递归至值1时停止递归,防止陷入死循环,是递归的边界条件。递归进行时,前阶段在不停调用自身,直至触及边界条件;触及边界条件后,进入返回阶段,将所得值逐次返回得出最终结果。

对于阶乘算法,我们也可以不使用递归算法,使用循环结构解决:

    public int factorial1(int n) {
        int result = 1;
        if (n <= 1){
            return result;
        }
        else {
            for (int i = n; i > 1; i--) {
                result *= i;
            }
            return result;
        }
    }

其运行结果一致。可见,递归算法用循环结构重构时,往往需要if选择结构(用于判断边界条件)和一个循环结构。但值得注意的是,循环结构能写成递归算法,但递归算法不一定能够写成循环结构(考虑到递归算法具有循环结构所不具备的“深度优先”特点)

3-5.2.2 反序输出正整数

在这个例子中,我们可以利用递归算法的深度优先将用户所输入的一串整数反序输出。

    public static void reverse(int num){
        int digit = num % 10;
        System.out.println(digit);
        if  (num / 10 != 0) {
            reverse(num/10);
        }
    }

同样的,这个例子也可以写成循环结构:

    public static void reverse1(int num) {
        while (num / 10 != 0) {
            System.out.print(num % 10);
            num /= 10;
        }
        System.out.println(num);
    }

运行结果如图所示:

image

3-5.2.3 斐波那契数列

斐波那契数列是由数学家莱昂纳多 · 斐波那契以兔子繁殖为例子引入的,又称“兔子数列”。其首两项均为1,随后项都是前两项之和,即:

\[ x_1 = x_2 = 1\\ x_n = x_{n-1} + x_{n-2} \]

    public static int finonacci(int n) {
        int x0 = 1;
        int x1 = 1;
        int x = x0 + x1;

        if (n <= 2) {
            return x0;
        }
        else {
            for (int i = 3; i < n ; i++) {
                x = finonacci(n-1) + finonacci(n-2);
            }
        }
        return x;
    }

3-5.2.5 汉诺塔问题

该部分参考自:

汉诺塔问题(分治+源码+动画演示) (biancheng.net)

汉诺塔问题(Hanoi)源自于印度一个古老的传说。印度教的“梵天之神”梵天创造世界时创造了三根石柱,其中一根石柱上自下向上以体积降序的顺序放置了64个圆盘。现要求一次只能移动一个位于顶部圆盘,最终让所有圆盘再排列顺序不变的情况下,从一个石柱转移到另一个石柱上。转移过程中允许使用其中一根石柱作为辅助。

首先,现在我们分别定义这三个石柱:

  • 源石柱(src:初始状态下所有圆盘最初所在石柱
  • 目标柱(tar:所有圆盘最终被转移的目标
  • 辅助柱(aux:转移过程中所需的辅助柱子

现在,设圆盘数量 = n

那么,我们先来看看n = 1时的情况:

image

这时,只需要直接将源石柱上的圆盘转移到目标柱计科。

接下来,我们再来看看n = 2时的情况:

image

现在,我们再来看看n = 3时的情况:

image

现在来看,当圆盘数量来到3时,操作次数已经达到了7,随着圆盘数量的不断增多,操作次数也会爆炸增长。

从上面的情况来看,可以发现,要想把圆盘全部转移,先把最大的圆盘之上的所有圆盘转移到辅助柱上,然后将最大盘转移至目标盘后,再将剩余的盘按照一样的方法转移至目标盘中,最终找到问题的解。

从这一点来看,当圆盘数量为n时,可以把问题拆分成n-1个圆盘和最后一个圆盘的转移问题,n-1个圆盘有能够继续往下拆分,直至n=1,从而满足递归算法。因此,我们可以用递归解决这一问题:

    public static int i = 1;

    public static void hanoi(int num, char src, char tar, char aux){
        if (num == 1) {
            //若只有一个圆盘:直接将其移动至目标柱
            System.out.println("第"+i+"次交换:将"+src+"上圆盘转移至"+tar);
            i++;
        }
        else {
            hanoi(num-1,src,aux,tar);       //将 n-1 个圆盘转移至辅助柱上
            System.out.println("第"+i+"次交换:将"+src+"上圆盘转移至"+tar);
            i++;
            hanoi(num-1,aux,tar,src);       //将辅助柱上的圆盘转移至目标住上
        }
    }
posted @ 2021-08-07 18:33  Zebt  阅读(102)  评论(0)    收藏  举报