算法学习-递归思想


这篇文章主要记录我对于递归思想的理解以及相关题目的思考,包括时间复杂度和空间复杂度的画图分析,以及递归优化的方向。

拿我来说,其实早在大一就接触到了递归,但是当时的能力也仅限于做对C++题目,其后在大二的汇编语言大作业上,又碰到了机器语言来实现递归,当时也由于自己编程能力太差,因此完成度不高,也埋下了对递归理解不深刻的祸根。如今,在自己精进算法,开始刷题的过程中,也碰到了很多涉及递归的算法题,初看题解通俗易懂,但是自己想独立写出来,却总是显得毫无头绪。

在网上多次寻求相关方面的文章,但无非都是从斐波那契数列、汉诺塔等问题讲起,但是理解了这些,却不足以让我应付更复杂的问题,如深搜、回溯等等。同时文章也多次强调“黑盒思想”,但是当我做题时,“黑盒思想”让我心里不踏实,总是觉得这样没法考虑完全题目,因此我决定写下这篇文章,尽力“说服自己”,在“不求甚解不踏实”和“钻入进去绕不出来”间,做个balance。

本文参考:

  1. 什么是递归,通过这篇文章,让你彻底搞懂递归
  2. 递归过程详解(Python)
  3. 告别递归算法,谈谈我的一些经验

递归定义与模板

简明扼要,直接摆出问题,

递归,就是自己调用自己,就是我们写的代码块里面又调用了自己,模板如下:

public void recursion(参数0) {
    if (终止条件) {
        return;
    }
    recursion(参数1);
}

其中需要注意三方面的内容:
1. 明确函数的作用
即我们要确认recursion()这个方法是要干什么的,它的返回值类型和传入参数是什么。这是我们后面运用“黑盒思想”的本源,就将它认定为只要我们输入合理,就能够正确产生我们想要的结果。在后面我们会碰到递归函数放在条件判断语句、将递归函数的结果先拿变量存起来再逻辑处理、直接返回递归函数等等形式,就是要深刻理解“黑盒思想”。
2.寻找递归结束条件
在递归函数中,如果一味的自己调用自己就会出现栈溢出,因此if (终止条件) return 非常重要。终止条件可能有多个,在相关的题目里面需要自己注意。许多文章中也把它称作base情况,即划分出来最小的那种情况。
3.找出递推公式
这个递推公式就是递归函数的主体,在方法里面调用自己,只要逻辑正确,能够进入下一步处理。形如在f(n)中,return n*f(n-1)if(f(n-1)==true) return true;int a=f(n-1,a); int b=f(n-1,b); return a&&b;这些例子里面都是在f(n)中找出了调用自己的递推公式。递归函数关注当前,剩下的交给子调用。

稍微复杂的模板如下,很多递归会调用自己多次,甚至在调用之前或之后都会进行一定的逻辑处理:

public void recursion(参数0) {
    if (终止条件) {
        return;
    }

    「可能有一些逻辑运算」
    recursion(参数1)
    
    「可能有一些逻辑运算」
    recursion(参数2)
            ……
    recursion(参数n)
    「可能有一些逻辑运算」
}

当调用自己太多的时候,会改成for循环的形式,在回溯的相关题目中经常见到这样的写法,如:

private void combinationSum(List<Integer> cur, int sums[], int target) {
    //终止条件必须要有
    if (终止条件) {
        return;
    }
    //逻辑处理(可有可无,视情况而定)
    for (int i = 0; i < sums.length; i++) {
        //逻辑处理(可有可无,视情况而定)
        //递归调用(递归调用必须要有)
        //逻辑处理(可有可无,视情况而定)
    }
    //逻辑处理(可有可无,视情况而定)
}

其中讲到了「回溯」这个关键词,出现在递归中,即在递归的返回过程中,我们要消除本次递归改变的全局变量对下一次递归的影响,因此要将改变的全局变量在本次递归以后复原。这里的「回溯」特指全局变量的复原,递归函数返回递归栈弹出不在这个范畴。相关的问题我会开一篇文章细说。

在递归中,我们还需要理解一下return的作用,无论是出现在终止条件中的return亦或是函数执行完后的return,都可以这样理解:将结果返回到上一级调用它的位置。逐级return,直到最开始调用的函数,就能得到最终结果。

递归的时间复杂度以及空间复杂度

举斐波那契数列的例子来说

int fibonacci(int i) {
       if(i <= 0) return 0;
       if(i == 1) return 1;
       return fibonacci(i-1) + fibonacci(i-2);
}

时间复杂度
时间复杂度为:递归次数 * 每次递归的时间复杂度,接近二叉树O(2^n),其中n为二叉树深度
空间复杂度为:递归深度 * 每次递归的空间复杂度,即调用栈深度O(n),其中n为二叉树深度
空间复杂度

递归可以优化的方向

1. 考虑重复计算

从上面可以看出,递归过程中会重复使用f(3)、f(2)等,因此我们可以用数组将之前算出来的结果保存,后面直接取用。

// 我们实现假定 arr 数组已经初始化好为-1了。
int f(int n){
    if(n <= 2){
        return n;
    }
    //先判断有没计算过
    if(arr[n] != -1){
        //计算过,直接返回
        return arr[n];
    }else{
        // 没有计算过,递归计算,并且把结果保存到 arr数组里
        arr[n] = f(n-1) + f(n-2);
        reutrn arr[n];
    }
}

2. 考虑是否可以从下往上递推

通过递推公式,时间复杂度降为了O(N)

public int f(int n) {
       if(n <= 2)
           return n;
       int f1 = 1;
       int f2 = 2;
       int sum = 0;

       for (int i = 3; i <= n; i++) {
           sum = f1 + f2;
           f1 = f2;
           f2 = sum;
       }
       return sum;
   }

相关题目

运用递归的题目很多,常见的有深度优先搜索(常用于图和树的遍历)、回溯等,因此我们需要灵活定义递归函数,不畏难。关于深度优先搜索、广度优先搜索、回溯等专题我都会陆续整理出来。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

互联网民工蒋大钊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值