看到标题点进来的朋友,大概率都被递归折磨过:明明代码就几行,读起来却像在绕迷宫;面试官笑眯眯让你手撕斐波那契数列,你背了十遍,一让写递归还是卡壳。别慌,递归不是靠天赋,而是靠“出口+递推关系”这两个支点。五分钟不至于骗你,但静下心看完这篇,你真的能亲手写出斐波那契的递归解法,还能顺带搞懂汉诺塔每一步是怎么走的、SVN里那个递归参数是怎么回事。我尽量用大白话讲清原理,全程带代码和调试思路,适合刚学数据结构的新手,也适合打算把递归彻底补一补的求职党。
1. 递归到底是个什么东西:先搞懂两大铁律
1.1 递归出口和递推关系:缺一不可
想理解递归,我建议先别急着背定义。你回想一下排队时想知道自己排第几:你问前面的人“你排第几?”,他也不知道,继续问前面;一路问到队首,队首说“我排第1”,然后这个答案再一层一层传回来,每个人在自己的编号上加1,最后你知道了自己排第8。这个过程就是递归。队首的“我排第1”是递归出口,每个后面的人都做同样的“加1”动作,是递推关系。两者缺一不可:没有出口,会一直问下去;没有递推关系,出口答了也没法把结果传回给你。
放到代码里,递归函数就是自己调用自己的函数。但“自己调用自己”这句话很害人,因为新手会想:“它调用自己的时候,不是应该重新跑一遍吗?那什么时候才停?”答案就是:靠两个条件控制。第一个条件是参数在不断变化,通常表现为规模变小,比如 n 变成 n-1;第二个条件是当参数到达某个特殊值时,函数直接返回一个确定的结果,不再继续调用。这两个条件合在一起,才是完整的递归。不少同学写递归漏了出口,或者出口写得太靠后,程序跑起来就是栈溢出,这是几乎所有人都会踩的第一个坑。
1.2 用“俄罗斯套娃”理解函数自己调用自己
俄罗斯套娃大家都见过:打开一个,里面还有一个更小的;再打开,还有更小的;直到最后一个实心的小娃娃,没法再打开了。递归函数其实就是一套“俄罗斯套娃逻辑”:你负责处理当前这一层,剩下的交给下一层递归调用,直到碰到那个“实心娃娃”(递归出口),再一层层把结果拼回去。
来看C++里最经典的入门例子,阶乘。n! = n * (n-1)!,而1! = 1。用递归写就是:
cpp复制int factorial(int n) {
if (n <= 1) {
return 1; // 递归出口:实心娃娃
}
return n * factorial(n - 1); // 当前层 * 剩余层
}
很多人卡在“factorial(n-1)到底怎么算出结果的”这个问题上。我的建议是,先把递归当成一个黑盒:你相信它一定算得对,只需要知道它返回的是(n-1)!,然后用 n 乘上它,就得到 n!。这种“信任子问题已经解决”的思路,是学会递归最重要的一步。不要试图在脑子里把递归一层层完整展开,展开到第三层你就会晕,展开到第五层就想放弃。学会假设、拼结果、写出口,你才能从递归小白变成会写递归的人。
1.3 函数调用栈:为什么递归没出口会崩
递归能成立,是因为底层有一块叫做“函数调用栈”的区域在支撑。每次函数调用,系统都会把当前函数的状态(参数、局部变量、返回地址)打包压入调用栈,等函数返回再弹出。递归调用本质也是普通的函数调用,只不过被调用的函数是自己。所以递归每加深一层,调用栈就多一层;如果一直不返回,栈就被压得越来越高,最后碰到系统给的栈空间上限,程序直接崩溃,也就是我们常说的“栈溢出”。
这个机制可以理
