递归这东西,我见过太多人卡在这一关上,包括当年的我自己。代码明明只有几行,看上去也"读得懂",可真要自己动手写一个,脑子就转不过弯来,总觉得自己跟电脑之间隔了层什么。直到后来我把函数调用栈彻底搞明白,再看递归忽然就通透了大半,那些花里胡哨的写法、一次性判断不准的边界条件,也都有了可以推导的依据。
这篇文章想做的事很简单:把递归从"玄学"变成"手艺"。我会从函数调用栈的底层机制讲起,带你把递归执行的每一步都用大脑和纸笔模拟一遍,然后通过阶乘、目录遍历、汉诺塔、斐波那契这四个从易到难的例子,逐步把递归三要素内化成肌肉记忆,最后再讲清楚性能优化和递归转迭代的工程选型问题。整个过程中涉及的核心知识点,只需要一个装了Python的电脑就能验证,编辑器用记事本或者VS Code都行,不需要装任何第三方库,这一点对初学者非常友好。
1. 递归的底层机制:函数调用栈里藏着答案
很多人学递归败在第一步,不是因为不知道"函数自己调用自己"这个定义,而是因为不知道这个定义在机器层面上到底是怎么发生的。脑子里只有一个模糊的"自己调自己"的概念,遇到稍微复杂一点的递归就抓瞎。所以第一节课,我们先把"函数调用"这个动作拆开来看。
1.1 函数调用时,Python在内存里做了什么
想象一下你在一家饭馆后厨工作,手上正切着配菜,突然接到一个电话说"帮我查一下冷藏库里还有几斤五花肉"。你放下手里的刀,记住自己正切到一半的位置,然后去冷库数了一遍,回来接着切。这个"记住切到一半的位置"的动作,在计算机世界里的名字叫保存现场。
Python中每一次函数调用,都会在内存中开辟一块名为"栈帧"的区域,里面保存了这个函数当前的所有局部变量、参数,以及最重要的——执行到哪一行了。这些栈帧按照调用的先后顺序一层层叠上去,形成调用栈。
python复制def outer():
x = 1
inner()
y = 2
def inner():
a = 10
print("inner")
outer()
上面这段代码执行到 inner() 时,内存里其实是两层栈帧:最底下是 outer 的栈帧,保存着 x=1 和执行到第4行的位置;上面是 inner 的栈帧,保存着 a=10。inner 执行完后,它的栈帧被销毁,程序才回到 outer 那块栈帧,继续执行 y=2。
递归之所以让人困惑,是因为每次"自己调用自己"时,都会生成一个全新的、独立的栈帧。第1次递归调用和第100次递归调用,虽然代码是同一段,但它们的内存空间是完完全全隔离的,各自拥有各自的参数和局部变量。
提示:这一点是用大脑模拟递归的关键——不要把一个函数想象成"一个"东西,要把它想象成"一份图纸"。递归每调用一次,就按同一张图纸重新盖一栋楼,每栋楼都是独立存在的。
1.2 "递"与"归":递归执行的两个阶段
理解了栈帧之后,递归的执行过程就很好描述了。一个递归调用,在时间轴上会经历两个截然不同的阶段。
递去阶段(也常叫"递推"):程序不断向深处调用自己,每一层的参数都在向着"终止条件"的方向变化。此时栈帧在不断增加,像叠盘子一样。
归来阶段(也常叫"回归"):当某一层的调用满足了终止条件,不再向下调用,而是返回一个具体的值。这个返回值会传递给上一层,上一层利用它算出结果,再继续向上返回。与此同时,栈帧从顶到底被一层层释放,像倒着收盘子。
python复制def factorial(n):
if n == 1:
return 1
return n * factorial(n - 1)
以 factorial(4) 为例,递去阶段会依次生成 factorial(4) → factorial(3) → factorial(2) → factorial(1) 四个栈帧。当执行到 factorial(1) 时,终止条件生效,直接返回1。然后进入归来阶段:
- 1返回给
factorial(2),计算出2 * 1 = 2 - 2返回给
factorial(3),计算出3 * 2 = 6 - 6返回给
factorial(4),计算出4 * 6 = 24
全过程下来,"递"叠了4层栈,"归"算了3次乘法。这就是递归的全貌:先向下开路,再向上收网。
1.3 为什么你常见到 RecursionError
几乎每个学递归的人都会在某个时刻收到这样一条报错:
text复制RecursionError: maximum recursion depth exceeded while calling a Python object
翻译过来就是"递归深度超出了上限"。Python解释器为了不让程序无限递归,把默认的递归深度限制在了1000层左右。一旦递去阶段叠的栈帧超过这个数字,解释器就会立刻出手掐断。
这个1000层的限制不是随便定的。每一个栈帧都要占用内存,深度太大时,内存会被瞬间耗尽。限制递归深度,本质上是给程序上了一道保险。
python复制import sys
print(sys.getrecursionlimit()) # 通常输出1000
我见过不少初学者在拿到RecursionError之后,第一反应是"我是不是递归写错了"。这是一个误区——大多数时候代码逻辑完全正确,只是数据规模超过了递归的物理极限。比如你用纯递归做第1000个斐波那契数,不是算法不对,而是递归本身撑不住这么深的调用链。这也引出了后文要讲的递归转迭代问题。
注意:如果你确实需要调高递归深度(比如某些分治算法),可以用
sys.setrecursionlimit(5000)手动设置,但这只能救一时,不能从根本上解决递归过深导致的性能问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 递归三要素:从"看得懂"到"写得出"
看别人写的递归,和亲自动手写递归,完全是两回事。前者的难点在阅读,后者的难点在"从一片空白里凭空构造出一个能自我调用的结构"。我教过的学生里,那些能稳定写对递归的人,没有一个是靠"灵感"写出来的,他们心里都有一套固定的分析框架。这就是我要在这里讲清楚的递归三要素。
2.1 终止条件:递归的命门
递归函数必须有一个明确的退出路径,否则就会无限调用直到栈溢出。这个退出路径通常是最小规模的子问题,答案能直接给出,不需要再递归。
以阶乘为例,最小的子问题是 factorial(1) = 1,所以终止条件是 if n == 1: return 1。以目录遍历为例,最小的子问题是"当前目录下没有任何子目录",只处理文件就返回。
判断终止条件写得对不对,有一个很笨但有效的办法:把这个函数的输入想象成"最小到不能再小"的样子。对数字来说,往往是0或1;对列表来说,往往是空列表;对树节点来说,往往是叶子节点。然后问自己:"如果我只把这个最小输入传进去,函数能不能在一层之内就返回一个正确结果?"如果能,终止条件基本就稳了。
2.2 问题拆分:把大问题降级成兄弟问题
这是递归三要素里最考验思维的一环。在一个规模为n的问题上,你得先找到它和规模为n-1(或者其他更小规模)的问题之间的"递推关系",确保每次递归都在朝终止条件逼近。
以 factorial(n) 为例,拆分逻辑是:n! = n * (n-1)!。大问题 factorial(n) 通过一次递归调用 factorial(n-1) 就把规模降了一阶,并且二者是等价关系,不是近似关系。
再举一个经典例子:求列表的和。
python复制def list_sum(arr):
if not arr:
return 0
return arr[0] + list_sum(arr[1:])
这里我们把"求n个数的和"拆成"第1个数 + 求剩下n-1个数的和"。递归调用传进去的 arr[1:] 比原列表少一个元素,规模必然在缩小。
需要注意,拆分过程中绝对值不变量是"递归调用的输入必须比当前输入更接近终止条件"。如果拆分的两个子问题规模差不多,或者拆着拆着又变回去了,递归就没法收敛,必然变成无限递归。
2.3 返回值合并:计算逻辑的关键一步
递归函数在归去阶段要做的事,就是把子问题的返回值,和当前层自己拥有的信息,整合成这一层的结果。
这一步是初学者最容易胡写的地方。很多人能写对终止条件和递归调用,但在 return 那一行怎么取巧都取不对。说到底,返回值合并取决于你的大问题和小问题之间是什么样的数学或逻辑关系。
阶乘:return n * factorial(n-1),是乘法合并。
列表求和:return arr[0] + list_sum(arr[1:]),是加法合并。
目录遍历:假设递归函数 scan(path) 返回 path 下的所有文件名,那么合并逻辑就是"当前目录内直接存放的文件 + 每个子目录调用 scan() 返回的所有文件",这里用的是列表拼接合并。
我强烈建议初学者在写递归之前,先在注释里写清楚两句话:这一层我要把子问题的结果跟什么东西合并?合并方式是什么? 把这两句话说清楚了,return 那一行基本不会错。
2.4 初学时调试递归的两个小工具
写递归有一个天然的缺点:不好调试。因为中间过程都藏在栈帧里,print丢进去之后你能看到一堆重复的函数调用,一时分不清当前是第几层。这里分享两个我自己实测非常顺手的调试技巧。
第一个是给函数加一个depth参数,用于标记当前的递归深度:
python复制def factorial(n, depth=0):
print(" " * depth + f"factorial({n}) 进入")
if n == 1:
print(" " * depth + "factorial(1) 返回 1")
return 1
result = n * factorial(n - 1, depth + 1)
print(" " * depth + f"factorial({n}) 返回 {result}")
return result
运行后你会看到充满缩进的调用树,一眼就能分辨哪次调用是哪一层发起的。第二个技巧是用内置的 traceback.print_stack() 在程序里临时打印当前的调用栈信息,适合排查"这个函数到底是被谁调进来的"这类问题。
3. 三个必须手写一遍的经典案例
理论讲完,得上真家伙。这三个案例我按难度做了排序,从纯数学到真实应用再到逻辑策略,每一个都能帮你巩固递归三要素里的某一环。
3.1 阶乘:最小可验证的递归骨架
阶乘是递归最经典的入门题,因为数学定义本身就是递归的:
text复制n! = n * (n-1)!
再看一遍三要素:终止条件是 n == 1;拆分是 factorial(n) 依赖 factorial(n-1);合并是乘法。
python复制def factorial(n):
"""
计算 n 的阶乘, n 为正整数
用法: factorial(5) -> 120
"""
if n == 1:
return 1
return n * factorial(n - 1)
这里有三个易错点值得单独提醒。
第一,终止条件必须是 n == 1 或 n == 0,不要写成 n == 2。如果你写成 n == 2,当传入3时, factorial(2) 不会进入终止分支,又会调用 factorial(1),而 factorial(1) 不满足任何条件,会继续调用 factorial(0)、factorial(-1),陷入死循环直到栈溢出。这种差错在初学时时有发生,根源在于你把"我直觉上觉得已经足够小了"当成了"算法上已经最小了"。
第二,注意递归函数不要忘了写 return。很多人写出了 n * factorial(n-1) 却没写 return,函数会返回 None,然后报 TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'。每次遇到这个报错,先检查递归调用外层有没有 return。
第三,阶乘虽然适合入门,但工程上没人用递归算阶乘,因为一个 for 循环就能搞定。它的最大意义在于提供一个极简模型,帮你验证递归代码的结构是否正确。
3.2 遍历嵌套目录:递归在实战中最常见的面孔
跟纯数学例子相比,目录遍历是离真实世界最近的递归练习题。你的操作系统里,文件目录天然就是一棵树:一个文件夹包含若干文件和若干子文件夹,每个子文件夹又包含若干文件和更深的子文件夹。树的形态决定了它和递归之间是天作之合。
python复制import os
def list_all_files(path):
"""返回 path 目录下所有文件的绝对路径列表"""
result = []
for item in os.listdir(path):
full_path = os.path.join(path, item)
if os.path.isfile(full_path):
result.append(full_path)
elif os.path.isdir(full_path):
result.extend(list_all_files(full_path))
return result
这段代码是"递归三要素"在工程场景下的标准范式:
- 终止条件:
os.path.isfile(full_path)为真,直接把文件加入结果,不再向下递归。 - 拆分:对每个子目录,调用
list_all_files(full_path),这就把"遍历一个大目录"拆成了"遍历若干个更小的子目录"。 - 合并:当前目录的直接文件列表,加上每个子目录遍历返回的结果,用
extend拼接。
我特别推荐初学者把这段代码亲手敲一遍,并拿到自己的Python安装目录或项目目录里跑一下,看看屏幕上列出的文件数量有多大。当你亲眼看到递归一层一层把几十层的嵌套目录完整列出来,那种"原来这就是递归"的通透感,比看什么都管用。
注意:windows下路径分隔符是反斜杠,直接用
os.path.join就能自适应系统差异,不要手写字符串拼接。
3.3 汉诺塔:用递归表达"策略"而非"步骤"
汉诺塔问题:有三根柱子A、B、C,A柱上有n个大小不同的圆盘,每次只能移动一个圆盘,小圆盘必须始终在大圆盘上面,目标是把所有圆盘从A移到C。
用循环写这种题会让人头发掉光,因为你要把移动过程拆成极其琐碎的指令序列,还要时刻记录状态。但用递归写,只需要一条非常漂亮的逻辑:
要把n个盘子从A移到C,等于先把n-1个盘子从A移到B,再把最大的盘子从A移到C,最后把n-1个盘子从B移到C。
python复制def hanoi(n, source, target, auxiliary):
"""
将 n 个盘子从 source 借助 auxiliary 移动到 target
"""
if n == 1:
print(f"移动盘子 1: {source} -> {target}")
return
hanoi(n - 1, source, auxiliary, target)
print(f"移动盘子 {n}: {source} -> {target}")
hanoi(n - 1, auxiliary, target, source)
调用 hanoi(3, 'A', 'C', 'B'),输出如下:
text复制移动盘子 1: A -> C
移动盘子 2: A -> B
移动盘子 1: C -> B
移动盘子 3: A -> C
移动盘子 1: B -> A
移动盘子 2: B -> C
移动盘子 1: A -> C
整个过程正好7步,符合 2^3 - 1 的公式。
汉诺塔的精华在于:递归让我们用"策略"写代码,而不是用"具体步骤"写代码。策略只关心三件大事——先把谁移到哪、再把当前盘子移到哪、最后把谁移到哪——而具体那n-1个盘子内部是怎么倒腾的,递归自动替你解决了。这种抽象能力,是递归在算法世界最大的价值,也是你从"能写递归"进阶到"懂递归思维"的分水岭。
初学汉诺塔时容易犯的错是分不清source、target、auxiliary三个角色。在 hanoi(n - 1, source, auxiliary, target) 这一行里,原来的辅助柱变成了这一次的终点柱,原来的终点柱变成了这一次的辅助柱。每一次递归调用都是一次全新的"角色重分配",这是整套代码最精妙、也最迷惑人的地方。如果你跟不下来,就回到第1节的方法,自己在纸上把 hanoi(3, 'A', 'C', 'B') 的调用栈画一遍,画完就通了。
4. 递归的性能分水岭:从斐波那契看重复计算
前面几个例子,递归的性能都还能接受,因为每个子问题只会被计算一次。但到了斐波那契数列,事情开始起变化。
斐波那契数列的递归定义非常诱人:
text复制f(0) = 0, f(1) = 1
f(n) = f(n-1) + f(n-2)
对应的代码也是三行就能写完:
python复制def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
如果你试着算 fib(40),你会发现程序明显卡顿,可能要等个几秒甚至十几秒。而 fib(50) 几乎会等到让人怀疑电脑死机。问题不在这段代码"递归"本身,而在于这段递归有严重的重复计算。
4.1 递归不一定慢,盲目递归一定慢
把 fib(5) 的调用关系的账算一笔就清楚了:
text复制fib(5)
= fib(4) + fib(3)
= (fib(3) + fib(2)) + (fib(2) + fib(1))
= ((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
...
同一个 fib(1) 被算了多少遍?答案是5遍。随着n增大,重复计算会按指数爆炸式增长。如果你用计数器统计一下 fib(30) 里 fib(1) 被调用了多少次,数字会让你吃惊。
反观我在3.1节里讲的阶乘,factorial(n) 对每个 factorial(k) 只会计算一次。同样是递归,阶乘是线性复杂度O(n),而朴素递归的斐波那契是O(2^n)。差距不是常数级别的,是指数与低阶之间的天壤之别。所以"递归性能差"这个黑锅,递归本身不背,背锅的是"重复子问题"。
4.2 记忆化优化:把算过的结果存起来
既然问题出在重复计算,那就让每个子问题的结果只算一次:算完存在字典里,下次调用先查一下,查到了直接返回。
python复制memo = {}
def fib_memo(n):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib_memo(n - 1) + fib_memo(n - 2)
return memo[n]
这样子调用 fib_memo(50) 时,整个计算过程的耗时肉眼几乎感知不到,因为每个 fib(k) 只被真正计算了一次,整体的计算次数从指数级降到了线性级。
Python里还有更简洁的写法,用内置的 functools.lru_cache 装饰器,几行代码一样的效果:
python复制from functools import lru_cache
@lru_cache(maxsize=None)
def fib_cache(n):
if n <= 1:
return n
return fib_cache(n - 1) + fib_cache(n - 2)
建议你两种方式都写一遍。手写memo那版能帮你理解"缓存"的作用过程;用装饰器那版让你以后在工程里遇到需要记忆化的递归时,能一眼认出更省事的路子。lru_cache本质上是同一个东西,只是它把存储和查询逻辑帮你封装好了而已。
提示:记忆化的代价是额外空间。一个斐波那契递归,最多要存n个键值对,空间复杂度是O(n)。大多数情况下n的量级在几万以内,牺牲这点内存换回指数级的时间节省,绝对划算。
4.3 尾递归的"名分"问题:为什么Python不帮你优化
聊递归优化,很难绕开"尾递归"这个词。所谓尾递归,就是递归调用是函数体中的最后一个操作,调用结束后函数直接返回该调用的结果,不再做任何计算。
拿阶乘举例,如果我把函数改成尾递归形式:
python复制def factorial_tail(n, acc=1):
if n == 1:
return acc
return factorial_tail(n - 1, acc * n)
这里靠一个额外参数acc(累加器)把中间结果一路带下去,当递归到达最深层时,整个结果已经算完,直接返回即可,不需要像前面那样在回归阶段逐层做乘法。很多函数式编程语言(比如Scheme)遇到这种写法会做"尾调用优化",直接把当前栈帧复用于下一次调用,从而把递归深度从O(n)降到O(1),彻底避开栈溢出问题。
但很遗憾,Python解释器没有实现尾递归优化。你在Python里写尾递归,该叠栈照样叠栈, factorial_tail(2000) 照样给你抛RecursionError。所以对这个优化手段,我的建议是:了解它的原理,心里有数就行,别指望在Python里用它规避性能问题。真要追求性能,看下一节。
5. 递归还是迭代:一个务实的选型判断
在Python里,递归和迭代从来不是"谁比谁高级"的较量。两者各有各的适用场景,工程里的优秀代码往往是二者搭配使用的结果。
5.1 递归的真实成本清单
选型之前,先看清递归的成本构成:
- 栈帧开销:每次递归调用都得在内存里分配一个栈帧,保存参数、局部变量和返回地址。深度为n的递归,大致需要n个栈帧的空间。
- 时间开销:创建和销毁栈帧本身就要花时间,比for循环体里的普通运算开销大不少。
- 栈溢出风险:一旦数据规模超过解释器设置的递归深度上限,程序直接崩溃。
- 代码可读性收益:递归的代码结构天然贴合树形或分形结构,能从策略层面描述问题,可读性往往远胜对应的迭代实现。
所以"用递归"换来的不是性能,是表达上的清晰和编码上的简洁。你要做的不是二选一,而是搞清楚当前场景下的"树"到底有多深、有没有重复子问题、能不能承受栈帧开销。
5.2 手动用栈模拟递归:递归转迭代的核心思路
有时候你确实喜欢递归的逻辑,但数据规模就是大到会触发RecursionError,这时候就该手动用"显式栈"来模拟递归了。思路非常简单:把每次递归调用变成一次"任务入栈",把函数返回变成"任务出栈"。
就拿目录遍历举例。前面递归版写法很优雅,但如果目录层级极深,比如嵌套了上万个文件夹,同样可能触发递归深度限制。这时可以把递归改写为迭代版:
python复制import os
def list_all_files_iterative(root):
"""用显式栈模拟递归, 规避递归深度限制"""
result = []
stack = [root]
while stack:
current_path = stack.pop()
for item in os.listdir(current_path):
full_path = os.path.join(current_path, item)
if os.path.isfile(full_path):
result.append(full_path)
elif os.path.isdir(full_path):
stack.append(full_path)
return result
这个版本没有递归调用,所以不存在栈溢出的问题。它把"还需要遍历的目录"放进一个栈里,while循环每次从栈里取一个目录出来处理,遇到子目录就放回栈里。整个过程和递归版在逻辑上完全等价,区别只是"待办事项"的存放位置从调用栈换到了你自己管理的一个列表里。
同理,汉诺塔和斐波那契也都能用显式栈或动态规划转成迭代版。我的练习建议是:挑一个你写过的递归函数,用显式栈改成迭代版,你会对"调用栈到底在做什么"产生一次质的理解飞跃。
注意:斐波那契这类带返回值合并的递归,转成迭代版时需要额外记录每个子问题的中间结果。最常见的做法是自底向上的动态规划,用一个数组从f(0)、f(1)一直推到f(n),这本质上就是给递归画了一张反向执行表。
5.3 我的选择标准:五个问题帮你做决定
实战里,我判断一个场景到底用递归还是迭代,一般会问自己五个问题:
- 问题有没有天然的递归结构?比如目录树、JSON嵌套、链表反转、分治算法——有树就有递归的用武之地。
- 数据规模会不会接近递归深度上限?如果一开始就知道有上万层,就别硬用递归。
- 有没有明显的重复子问题?有,就该加记忆化,或者干脆改成动态规划。
- 代码的可读性收益能否抵过性能损失?有些递归版本一眼就能看懂逻辑,而对应的迭代版本可能要花10分钟捋清思路,那就值得用递归。
- 团队/项目有没有强制约定?有些团队对递归深度有明确限制,这是硬约束,得先看文档再动手。
说到底,递归和迭代是同一种逻辑的两种表达。递归是"我告诉电脑我想做什么",迭代是"我告诉电脑怎么一步步做"。二者各取所长,才是一个有经验的Python工程师该有的姿态。
6. 学习递归的路径和几个常见误区
最后一个部分,聊聊学习路上最常绊倒人的几个认知误区,以及一条实际验证过的进阶路径。
6.1 最大误区:以为递归是循环的变体
这是我见过最广泛、也最耽误事的看法。"递归不就是一种写循环的方式吗?"——如果你抱着这个念头学递归,大概率会卡住,因为你总会试图用循环的思维方式去分析递归:考虑初始值、考虑步长、考虑什么时候退出循环。这一套在递归里统统不适用。
递归的思维方式是自顶向下的:先假设子问题已经解决了,然后通过某种合并逻辑把大问题拼接出来。你不是在"一步步执行",你是在"声明一个问题的分解方案"。
一个很实用的思维转换训练:拿到任何递归代码,先别急着运行,也别一行行模拟。先在注释里用一句话写出"这个函数接收什么、返回什么、内部怎么拆";再想"如果这个函数内部的所有递归调用都已经能正确返回,我该怎么利用它们写出当前这层逻辑"。一旦你习惯了这个思路,递归就变成了一种搭积木游戏。
6.2 递归是"自上而下"还是"自下而上"?搞混了方向
递归过程从第一层一直调到底层,再从底层一路返回,表面上看起来既有"从上到下"的递去,又有"从下到上"的归来。初学者最迷惑的是:那一刻到底该用上一层的结果,还是用下一层的结果?
这里我提供一个便于记忆的判断方法。函数的返回值永远是"从最深处开始产生的"。最先算出来的是最小的子问题,它的结果会被上一层拿来用。因此在代码里,越靠近递归终止条件的逻辑,越先执行;越靠近调用入口的逻辑,越后执行。
如果你设计一个递归函数,想把"每一层都输出点什么",并且希望输出顺序是从第1层到第n层,那就在递归调用之前print;如果你希望输出顺序是从第n层到第1层,那就在递归调用之后print。这个"递归调用前后"的位置关系,决定了逻辑的先后顺序,也是很多人栽跟头的地方。
6.3 适合练递归的后续方向
如果前面那几个案例你都能独立写出来,推荐按这个顺序继续加固:
- 链表操作:反转链表、合并两个有序链表,可以帮你理解递归在"非线性结构"上的写法。
- 树的遍历:前序、中序、后序遍历,二叉树的最大深度,这是递归最舒服的舞台,也是面试高频题。
- 分治算法:归并排序、快速排序。这两种排序的核心思想就是"拆到不能再拆,分别处理,再合并"——递归三要素的完美范本。
- 回溯法:全排列、组合总和、N皇后。这是递归的进阶应用,难点在于"撤销选择"这一操作,对初学阶段来说可以留到递归基础扎实之后再去挑战。
每一步都别贪多。宁可把一个案例从"照着写"练到"闭着眼也能写出来",也不要一口气刷十个似懂非懂的题。
根据我个人的实际教学和带项目经验,练递归这件事,最忌讳的就是只看不写。你在纸上推演调用栈一百遍,不如亲手敲一遍代码、再故意把某个条件写错、观察报错信息、然后修正回来——这样一整套下来,学到的东西才真正是你的。建议你从今天的目录遍历改写开始,先把递归版写顺,再按5.2节改成迭代版,两边一对比,递归的理解深度会上一个台阶。后续遇到真正复杂的树形数据处理场景,你就能游刃有余地判断该走哪条路了。
