在我们刚刚接触编程的时候,递归可以说是我们接触的第一个难点吧,前面学过的if else while 函数什么的都不会很难。而递归这个东西有点绕,初学者如果逻辑不清晰,可能会被绕进去。那么今天我就来讲讲关于递归的那些事儿。
在讲递归之前我们先来看一下大家熟悉的循环,无论是for循环、while循环,或者是其他的什么牛马循环,都有一些共同的特点,那就是有循环结束的状态 和 一个状态的改变,如果没有这两个东西,循环就会变成一个死循环,我们可以看看下面的一个代码片段,结束的状态是 i<100这个条件不满足的时候,也就是说i == 100的时候循环结束,而状态的改变是每循环一次i的值加1,这应该很好理解吧。
#include<stdio.h>
int main(){
int sum = 0;
for(int i = 0;i<100;i++){
sum = sum + i;
}
printf("%d",sum);
return 0;
}
现在我们再来看看递归:程序调用自身的编程技巧称为递归,这是官方的说法,简单说就是函数自己调用自己,就类似于下面代码段。
#include<stdio.h>
void func(){
printf("hello,world");
func();
}
int main(){
func();
return 0;
}
这行代码虽然简单,但是存在一个非常严重的错误,func函数会一直自己调用自己,就像狗咬尾巴转圈圈一样,直到堆栈溢出为止,不信邪的同学可以动手敲一个试试看。
我们应当避免这种陷阱,想想前面说的循环的特点循环结束的状态,状态的改变套用到递归这里就是递归结束的状态,递归变量状态的改变以及传递大家不理解我可以举一个例子,我把上面循环的代码改成迭代的版本;
#include<stdio.h>
int addFunc(int i){
if(i >= 99) return i;
int res = i+addFunc(i+1);
return res;
}
int main(){
printf("%d",addFunc(0))
return 0;
}
效果和循环的代码是一样的,这个递归就是安全的递归,因为存在两个因素。
递归结束的状态:i >= 99 时不再调用自己了,而是直接返回自己。
传递改变的状态:如果不满足i >= 99 ,先递归调用自己,不过参数i的值加1,等调用函数返回的时候再加上 i最后再返回结果。
看到这里大家应该知道如何写出正确的迭代代码了吧!
在什么时候用递归呢?也许你对递归理解了,也知道递归的过程了,但是却不知道应该在什么时候使用递归,这可能也是大多数人的困惑吧,为了解决大家的困惑我决定先举几个例子给大家看看,然后再总结。
第一个例子就是第n个斐波那契数,废话少说,直接上代码:
#include<stdio.h>
int fib(int n){
if(n == 1 || n == 2){
return n;
}else{
int res = fib(n-1) + fib(n -2);
return res;
}
}
int main(){
printf("%d",fib(100));
return 0;
}
代码的意图是计算第100个斐波那契数并输出,下面是第n个斐波那契数的定义,当我们要求第n个(n > 2)斐波那契数的时候,要先求出第n-1,第n-2个,一直递归下去,直到n == 1 或者 n == 2;
第二个例子就是二叉树的递归遍历,代码如下;
public void recursive(TreeNode root){
if(root == null) return;
System.out.println(root.val);//先序遍历
recursive(root.left);
//在这里输出就是中序遍历
recursive(root.right);
//在这里输出就是后序遍历
}
下图是二叉树的一般结构,要遍历二叉树其实就分为3步,遍历root根结点,遍历左子树,遍历右子树,而遍历左右子树又可以分为3步…
当遍历到root为空时直接返回。
例子还有很多,有这2个就能够看出递归的特性了:规模更小但是结构或者解决步骤相同的子问题(也就是套娃)
讲到这里大家应该差不多可以理解了,什么时候可以使用递归了,当我们在解决的问题存在一种状态转移并且这种状态转移的规则相同的时候,就可以使用递归解决,如果学过算法应该听说过解决动态规划的时候有一个步骤是确定状态转移方程,而这也说明了,使用递归也可以解决动态规划的问题,不过这种解决的方法是非常暴力的,往往其时间复杂度以及空间复杂度都会非常大的,但是我们可以先写出暴力递归的代码,确定其中的状态转移过程,然后再由暴力递归代码改成循环求解的过程,如果大家感兴趣后面可以再写一篇博客详细介绍动态规划的博客。
递归在数据结构以及算法领域都是一个很重要的部分,相对于算法数据结构的其他内容,递归还是比较好理解的,下面总结递归调用的转移点;
- 递归调用往往要确定一个递归结束的时候,有的地方称之为BaseCase基本情况;
- 要存在状态改变的地方,如果没有状态改变每次都递归一样的参数,就变成循环递归了,
- 一般确定解决问题的状态转移方式,就可以写出递归代码(理解套娃的含义)