定义
递归是指一个函数在其定义中直接或间接地调用自身的方法。通过这种方式,函数可以将一个复杂的问题分解为规模更小的、与原问题相似的子问题,然后通过不断地解决这些子问题来最终解决整个问题。
组成部分
递归主体
这是函数中递归调用自身的部分,它通过将问题分解为更小的子问题来逐步解决整个问题。
递归终止条件
也称为基本情况,是递归函数停止调用自身的条件。当满足这个条件时,函数不再进行递归调用,而是直接返回一个已知的结果。如果没有正确的终止条件,递归函数将无限循环,导致栈溢出等错误。
递归的层层展开和回溯过程
层层展开过程
层层展开指的是递归函数不断调用自身,问题规模逐步缩小,直至达到递归终止条件的过程。
逐步回溯过程
逐步回溯是指递归函数在达到终止条件后,依次返回上一层调用函数,继续执行后续代码的过程。最终回溯到初始调用。
递归函数中多次递归调用
递归函数中多次递归调用是依次进行的,每次调用都会创建新的栈帧压入调用栈,直到遇到递归终止条件后开始逐步返回,栈帧也依次弹出,返回到上一层调用函数中继续执行后续代码。这样就实现了递归的层层展开和逐步回溯的过程。
总结
递归的层层展开过程是不断将原问题分解为规模更小的子问题,直到达到递归终止条件;而逐步回溯过程则是在子问题解决后,依次返回上一层,利用子问题的结果解决更大规模的问题,最终得到原问题的解。整个过程通过调用栈来管理函数的调用和返回顺序,确保程序的正确执行。
生动理解
当你收到一份快递的时候,你打开快递包装,发现还有包装,再打开包装,发现还有包装,再打开包装,......…终于,你收到了一张字条,上面写着“我好饿啊,你干嘛要吃牛排意大利面?cnm”。此时,你感到很惭愧,不该在这么晚发表美食,于是,你写了一张抱歉信,然后你将快递重新包装回去,包装一层,再包装一层,........…,终于你包装完了,将快递按原地址发送回去。
(该解释来自acwing中的房智玄)
举例
以汉诺塔问题的递归函数为例:
void dfs(int n, char x, char y, char z)
{if(n==0) return; // 基本情况:没有盘子需要移动dfs(n-1, x, z, y); // 递归步骤1:将n-1个盘子从x移动到y,使用z作为辅助printf("%c->%d->%c\n", x, n, z); // 打印当前步骤:将第n个盘子从x移动到zdfs(n-1, y, x, z); // 递归步骤2:将n-1个盘子从y移动到z,使用x作为辅助
}
假设我们调用 dfs(3, 'A', 'B', 'C') ,下面详细阐述其层层展开和逐步回溯的过程。
层层展开过程
初始调用
我们调用 dfs(3, 'A', 'B', 'C') ,系统为该函数创建一个栈帧并压入调用栈。栈帧包含参数 n = 3 , x = 'A' , y = 'B' , z = 'C' 以及返回地址等信息。此时函数要解决的问题是把 3 个盘子从柱子 A 借助柱子 B 移动到柱子 C 。
第一次递归展开
在 dfs(3, 'A', 'B', 'C') 中,由于 n != 0 ,执行 dfs(n - 1, x, z, y) ,也就是 dfs(2, 'A', 'C', 'B') 。
系统为 dfs(2, 'A', 'C', 'B') 创建新的栈帧并压入调用栈。此时问题变为把 2 个盘子从柱子 A 借助柱子 C 移动到柱子 B 。
第二次递归展开
在 dfs(2, 'A', 'C', 'B') 中,因为 n != 0 ,执行 dfs(n - 1, x, z, y) ,即 dfs(1, 'A', 'B', 'C') 。
系统为 dfs(1, 'A', 'B', 'C') 创建新的栈帧并压入调用栈。现在问题是把 1 个盘子从柱子 A 借助柱子 B 移动到柱子 C 。
第三次递归展开
在 dfs(1, 'A', 'B', 'C') 中,由于 n != 0 ,执行 dfs(n - 1, x, z, y) ,也就是 dfs(0, 'A', 'C', 'B') 。
系统为 dfs(0, 'A', 'C', 'B') 创建新的栈帧并压入调用栈。此时问题规模缩小到 0 个盘子的移动。
达到终止条件
在 dfs(0, 'A', 'C', 'B') 中,因为 n == 0 ,满足递归终止条件,函数不再进行递归调用,准备开始回溯。
逐步回溯过程
第一次回溯
dfs(0, 'A', 'C', 'B') 执行 return 语句,其栈帧从调用栈中弹出。
程序控制流回到 dfs(1, 'A', 'B', 'C') 中调用 dfs(0, 'A', 'C', 'B') 的下一行代码,即 printf("%c->%d->%c\n", x, n, z); ,输出 A->1->C ,表示将编号为 1 的盘子从柱子 A 移动到柱子 C 。
第二次递归调用(在 dfs(1, 'A', 'B', 'C') 中)
接着执行 dfs(n - 1, y, x, z) ,即 dfs(0, 'B', 'A', 'C') 。
系统为 dfs(0, 'B', 'A', 'C') 创建新的栈帧并压入调用栈。
再次达到终止条件并回溯
在 dfs(0, 'B', 'A', 'C') 中,因为 n == 0 ,执行 return 语句,其栈帧从调用栈中弹出。
回到 dfs(1, 'A', 'B', 'C') 中调用 dfs(0, 'B', 'A', 'C') 的下一行代码,此时 dfs(1, 'A', 'B', 'C') 函数执行完毕,其栈帧从调用栈中弹出。
继续回溯到 dfs(2, 'A', 'C', 'B')
回到 dfs(2, 'A', 'C', 'B') 中调用 dfs(1, 'A', 'B', 'C') 的下一行代码,即 printf("%c->%d->%c\n", x, n, z); ,输出 A->2->B ,表示将编号为 2 的盘子从柱子 A 移动到柱子 B 。
接着执行 dfs(n - 1, y, x, z) ,即 dfs(1, 'C', 'A', 'B') ,重复上述递归展开和回溯的过程。
最终回溯到初始调用
经过多次递归展开和回溯,最终回到 dfs(3, 'A', 'B', 'C') 中调用 dfs(2, 'A', 'C', 'B') 的下一行代码,输出 A->3->C ,表示将编号为 3 的盘子从柱子 A 移动到柱子 C 。
再执行 dfs(n - 1, y, x, z) ,完成所有盘子的移动操作, dfs(3, 'A', 'B', 'C') 函数执行完毕,调用栈为空。