这篇文章主要记录我对于递归思想的理解以及相关题目的思考,包括时间复杂度和空间复杂度的画图分析,以及递归优化的方向。
拿我来说,其实早在大一就接触到了递归,但是当时的能力也仅限于做对C++题目,其后在大二的汇编语言大作业上,又碰到了机器语言来实现递归,当时也由于自己编程能力太差,因此完成度不高,也埋下了对递归理解不深刻的祸根。如今,在自己精进算法,开始刷题的过程中,也碰到了很多涉及递归的算法题,初看题解通俗易懂,但是自己想独立写出来,却总是显得毫无头绪。
在网上多次寻求相关方面的文章,但无非都是从斐波那契数列、汉诺塔等问题讲起,但是理解了这些,却不足以让我应付更复杂的问题,如深搜、回溯等等。同时文章也多次强调“黑盒思想”,但是当我做题时,“黑盒思想”让我心里不踏实,总是觉得这样没法考虑完全题目,因此我决定写下这篇文章,尽力“说服自己”,在“不求甚解不踏实”和“钻入进去绕不出来”间,做个balance。
本文参考:
递归定义与模板
简明扼要,直接摆出问题,
递归,就是自己调用自己,就是我们写的代码块里面又调用了自己,模板如下:
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;
}
相关题目
运用递归的题目很多,常见的有深度优先搜索(常用于图和树的遍历)、回溯等,因此我们需要灵活定义递归函数,不畏难。关于深度优先搜索、广度优先搜索、回溯等专题我都会陆续整理出来。