六, 递归以及几个典型的Java递归例子(待补充)

6.1 什么是递归?

定义:

递归就是方法自己调用自己,每次调用时传入不同的变量。递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。

用大白话来讲述递归, 知乎的一位大佬讲的就非常形象:

  • 一个小朋友坐在第10排,他的作业本被小组长扔到了第1排,小朋友要拿回他的作业本,可以怎么办?他可以拍拍第9排小朋友,说“帮我拿第1排的本子”,而第9排的小朋友可以拍拍第8排小朋友,说“帮我拿第1排的本子”…如此下去,消息终于传到了第1排小朋友那里,于是他把本子递给第2排,第2排又递给第3排…终于,本子到手啦!这就是递归,拍拍小朋友的背可以类比函数调用,而小朋友们都记得要传消息、送本子,是因为他们有记忆力,这可以类比栈.

递归算法解决问题的特点:

  1. 递归就是方法里调用自身
  2. 在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。
  3. 递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。所以一般不提倡用递归算法设计程序。
  4. 在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等,所以一般不提倡用递归算法设计程序。

递归的三个要点:

  1. 递归必要有明确的出口

    • 在使用递归算法时, 必须找到递归的出口. 也就是说,一个递归算法, 必须要有一个明确的递归结束条件, 当我们递进到一个特定的程度时候,如果这时候满足了递归结束条件, 就不再继续递进, 而是一步步的归来, 返回到自身方法的调用处(递归入口).
  2. 递归使用栈来实现的

    • 方法调用自身进行递归时会使用栈来保存临时变量.每调用一次方法自身, 都会将临时变量封装成栈帧压入内存栈, 等递进结束而该归来的时候,才进行出栈返回操作. 所以说, 如果递归求解的数据规模很大, 调用层次很深, 不断的递进,不断的入栈, 就会有堆栈溢出的风险.
  3. 递归分为递进和归来[相当重要, 务必反复阅读此段]

    • 递归是不断在方法体内调用自身方法的过程, 在我们满足递归结束条件(即到达递归出口)之前, 调用自身方法的这行代码(即递归入口)之前的代码段, 会随着每一次递归都得到执行, 这些过程都是递进的过程, 也可以说是入栈的过程.
    • 当满足了递归结束条件之后, 我们会从递进转为归来, 递归归来的顺序是它前行顺序的逆序, 类似于出栈一样, 归来过程中会按最后一次递进执行的结果向着第一次递进执行结果的顺序进行递进结果的返回. 在归来的过程中, 我们往往很容易忽视了一件事, 那就是随着每一次归来, 递归入口代码行下面的代码段都会被执行一次!

通过一段视频来理解上面这一段话吧, 请务必拿出纸仔细画

6.2 手写递归的三个要素

精炼总结:

  1. 实现什么功能?
  2. 递归出口是什么?
    3. 写出递推式子, 这个式子要能够逐渐逼近递归出口

6.2.1 第一要素: 明确你这个函数想要干什么

对于递归, 我觉着很重要的一个事就是, 这个函数的功能是什么, 他要完成什么样的一件事,由于递归的特点是问题和子问题都会调用函数自身,所以这个函数的功能一旦确定了, 之后只要找寻问题与子问题的递归关系即可.

比如, 我定一个函数

//计算n的阶乘(假设n不为0)
int f(int n){
    // f(n)= 1*2*3*4....*n-1*n;
}

这个函数的功能是算 n 的阶乘。好了,我们已经定义了一个函数,并且定义了它的功能是什么,接下来我们看第二要素。

6.2.2 第二要素: 寻找递归结束的条件

所谓递归,就是会在函数内部代码中,调用这个函数本身,所以,我们必须要找出递归的结束条件,不然的话,会一直调用自己,进入无底洞。也就是说,我们需要找出当参数为啥时,递归结束,之后直接把结果返回,请注意,这个时候我们必须能根据这个参数的值,能够直接知道函数的结果是什么。

例如,上面那个例子,当 n = 1 时,那你应该能够直接知道 f(n) 是啥吧?此时,f(1) = 1。完善我们函数内部的代码,把第二要素加进代码里面,如下

// 算 n 的阶乘(假设n不为0)
int f(int n){
    if(n == 1){
        return 1;
    }
}

有人可能会说,当 n = 2 时,那我们可以直接知道 f(n) 等于多少啊,那我可以把 n = 2 作为递归的结束条件吗?

当然可以,只要你觉得参数是什么时,你能够直接知道函数的结果,那么你就可以把这个参数作为结束的条件,所以下面这段代码也是可以的。

// 算 n 的阶乘(假设n>=2)
int f(int n){
    if(n == 2){
        return 2;
    }
}

注意我代码里面写的注释, 如果假设 n >= 2, 那么 n = 1时,会被漏掉,所以为了更加严谨,我们可以写成这样:
当 n <= 2时,f(n) = n,

// 算 n 的阶乘(假设n不为0)
int f(int n){
    if(n <= 2){
        return n;
    }
}

6.2.3 第三要素: 找出函数的等价关系式(递推式)

第三要素是, 我们要正式的通过调用自身方法来实现递归, 同时还要不断缩小参数的范围以逼近递归的结束条件`, 缩小之后, 我们还要注意不能改变原函数要实现的结果;

例如,f(n) 这个范围比较大,我们可以让 f(n) = n * f(n-1)。这样,范围就由 n 变成了 n-1 了,范围变小了,并且为了原函数f(n) 不变,我们需要让 f(n-1) 乘以 n。
说白了,就是要找到原函数的一个等价关系式,f(n) 的等价关系式为 n * f(n-1),即 f(n) = n * f(n-1)。
找出了这个等价,继续完善我们的代码,我们把这个等价式写进函数里。如下:

// 算 n 的阶乘(假设n不为0)
int f(int n){
    if(n <= 2){
        return n;
    }
    // 把 f(n) 的等价操作写进去
    return f(n-1) * n;
}

6.3 栗子的磨练

1. 递归打印n次字符串

public class OutpurStringRecur {
    static String str = "hello, world";
    //1.明确函数要做什么: 打印n次字符串
    public static void outputStr(int n){

        //2. 寻找递归结束条件:  n=0时,我们退出程序即可
        if(n == 0) {
            return;
        }
        System.out.println(str); //递归一次,打印一次
        //3. 如何逼近递归结束条件: 打印一次, n-1
        outputStr(n-1);  
    }

    public static void main(String[] args) { outputStr(4);}
}

2. 实现n的阶乘

public class basicFactorial {
    ///1. 目的是什么: 实现n的阶乘
    public static int getFactorial(int n){
        //2. 递归结束条件: 自顶向下,n递归一次减少1, 当 n=2,n=1时, 皆可以返回n
        if(n<=2) return n;
        //3. 递推公式以及如何逼近递归条件
        return n*getFactorial(n-1);
    }

    public static void main(String[] args) {
        System.out.println(getFactorial(4));}
}

3. 数组求和

public class ArrSumRecursion {


    public static int arrSum(int[] arr,int n){
        //自顶向下递归, 数组的索引不断减小
        if(n==1) return arr[0];
        //
        return arr[n-1]+arrSum(arr,n-1);
    }

    public static void main(String[] args) {
        int[] arr = {1,2,3};
        System.out.println(arrSum(arr,3));
    }
}

4. 斐波那契数列

public class Fibonacci {
    //1. 函数功能: 返回n对应的斐波那契书,
    public static int getFibonacci(int n) {
        //2. 递归结束条件, 因为是自顶向下的, n随着递归不断减小
        // n=1 f(1)=1, n=2 f(2)=1;
        if (n == 1 || n == 2) return 1;

        return getFibonacci(n - 1) + getFibonacci(n - 2);
    }

    public static void main(String[] args) {
        //
        System.out.println(getFibonacci(5));

    }
}

5.

栗子: 递归打印n次字符串斐波那契, 青蛙跳台阶, 求数组元素最大值 数组求和 阶乘, 二叉树的遍历 回文字符串的判断 反转链表 字符串的全排列 求二叉树的深度 归并排序 堆排序 迷宫问题 汉诺塔

  • 参考资料:
  • 好多, 好多, 谢谢大佬们(o(╥﹏╥)o)
posted @ 2022-05-26 20:31  青松城  阅读(353)  评论(0编辑  收藏  举报