小编怀着激动的心情编写本篇小博客,因为我要介绍的是递归——一种优雅的问题解决方法。递归将人分成三个截然不同的阵营:恨它的、爱它的以及恨了几年后又爱上它的。
希望各位读者在阅读小编的文章后,可以深刻理解递归思想。
为了让读者形象地认识到递归,先看一组漫画。
1、假设你在玩密室逃脱时,发现一个宝箱
3、这个盒子里有盒子,而盒子里的盒子又有盒子。钥匙就在某个盒子中。为了找到钥匙,苦逼的你尝试了不同的方法:
第一种方法:
(1)创建一个要查找的盒子堆。
(2) 从盒子堆取出一个盒子,在里面找。
(3) 如果找到的是盒子,就将其加入盒子堆中,以便以后再查找。
(4) 如果找到钥匙,则大功告成!
(5) 回到第二步。
第二种方法:
(1) 检查盒子中的每样东西。
(2) 如果是盒子,就回到第一步。
(3) 如果是钥匙,就大功告成!
在代码中,我们会发现,第一种方法就是while循环,第二种方法时函数调用自己。
两个方法都不差,但是欢Leigh Caldwell在Stack Overflow上说的一句话:“如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要”。
递归其实是⼀种解决问题的⽅法,在C语⾔中,递归就是函数⾃⼰调⽤⾃⼰。
举一个史上最简单的递归例子:
#include
int main()
{
printf("hehe\n");
main();//main函数中⼜调⽤了main函数
return 0;
}
上述就是⼀个简单的递归程序,只不过上⾯的递归只是为了演⽰递归的基本形式,不是为了解决问题,代码最终也会陷⼊死递归,导致栈溢出。
递归思想就是将大事化小的过程。
由于递归函数调用自己,因此编写这样的函数时很容易出错,进而导致
无限循环。
递归在书写的时候,有2个必要条件:
• 递归存在限制条件,当满⾜这个限制条件的时候,递归便不再继续。
• 每次递归调⽤之后越来越接近这个限制条件。
公式:n! = n ∗ (n − 1)!
C code
# define _CRT_SECURE_NO_WARNINGS
#include
int fact(int n) {
if (n <= 0) {
return 1;
}
else {
return n * fact(n - 1);
}
}
int main() {
int n = 0;
scanf("%d", &n);
int ans = fact(n);
printf("%d", ans);
return 0;
}
红色箭头表示递推,只是将大事化小,并没有计算出结果;绿色箭头表示回归,计算结果返回上一级。
输⼊⼀个整数n,打印这个按照顺序打印整数的每⼀位。
C code
# define _CRT_SECURE_NO_WARNINGS
#include
void Print(int n) {
if (n > 9) {
Print(n / 10);
}
printf("%d ", n % 10);
}
int main() {
int n;
scanf("%d", &n);
Print(n);
return 0;
}
Print(n)
如果n是1234,那表⽰为
Print(1234) //打印1234的每⼀位
其中1234中的4可以通过%10得到,那么
Print(1234)就可以拆分为两步:
1. Print(1234/10) //打印123的每⼀位
2. printf(1234%10) //打印4
完成上述2步,那就完成了1234每⼀位的打印
那么Print(123)⼜可以拆分为Print(123/10) + printf(123%10)
以此类推
Print(1234)
==>Print(123) + printf(4)
==>Print(12) + printf(3)
==>Print(1) + printf(2)
==>printf(1)
在C语⾔中每⼀次函数调⽤,都要需要为本次函数调⽤在栈区申请⼀块内存空间来保存函数调⽤期间的各种局部变量的值,这块空间被称为运⾏时堆栈,或者函数栈帧。
函数不返回,函数对应的栈帧空间就⼀直占⽤,所以如果函数调⽤中存在递归调⽤的话,每⼀次递归函数调⽤都会开辟属于⾃⼰的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采⽤函数递归的⽅式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack over flow)的问题。
事实上,我们看到的许多问题是以递归的形式进⾏解释的,这只是因为它⽐⾮递归的形式更加清晰,但是这些问题的迭代实现往往⽐递归实现效率更⾼。
当⼀个问题⾮常复杂,难以使⽤迭代的⽅式实现时,此时递归实现的简洁性便可以补偿它所带来的运⾏时开销。
有时候,递归虽好,但是也会引⼊⼀些问题,所以我们⼀定不要迷恋递归,适可⽽⽌就好。