一、递归初探:什么是递归?

说实话,我第一次接触递归时,整个人是懵的。

一个函数调用它自己?这不就是死循环吗?

后来我明白了——递归不是绕圈圈,而是层层递进,再层层返回。

1.1 生活中的递归现象

先别急着看代码。我们看看身边的世界。

俄罗斯套娃——你打开一个大娃娃,里面有个小一点的。再打开,还有更小的。直到最后一个实心的小娃娃,再也打不开了。

这个过程,就是递归:

  • 打开娃娃 → 发现里面还有娃娃 → 继续打开
  • 直到遇到最小的娃娃(终止条件)
  • 然后一层层往回走

两面镜子对着照——你站在中间,会看到无限个自己。每个镜子里的影像,都包含另一个镜子的影像。

这也是递归。只不过它没有终止条件,所以是无限递归——程序里这叫栈溢出。

核心思想:递归就是把一个大问题,拆成若干个与原问题结构相同但规模更小的子问题。

1.2 递归的数学基础:数学归纳法

递归和数学归纳法,其实是同一件事。

数学归纳法两步走:

  1. 基础步骤:证明 n=1 时成立
  2. 归纳步骤:假设 n=k 时成立,证明 n=k+1 时也成立

递归也是两步:

  1. 终止条件:处理最简单的情况(比如 n=0 或 n=1)
  2. 递归步骤:假设小规模问题已解决,解决当前问题

我当年学数据结构时,老师说过一句话,我一直记着:「你不需要关心递归的每一层是怎么执行的,你只需要相信它能正确返回。」

我的习惯:写递归函数时,先写终止条件。终止条件写不对,后面全是白搭。

1.3 一个最简单的递归例子

计算 n 的阶乘。n! = n × (n-1) × (n-2) × ... × 1

def factorial(n):
    # 终止条件
    if n == 0 or n == 1:
        return 1
    # 递归步骤
    return n * factorial(n - 1)

print(factorial(5))  # 输出 120

运行过程是这样的:

factorial(5)
= 5 * factorial(4)
= 5 * (4 * factorial(3))
= 5 * (4 * (3 * factorial(2)))
= 5 * (4 * (3 * (2 * factorial(1))))
= 5 * (4 * (3 * (2 * 1)))
= 5 * (4 * (3 * 2))
= 5 * (4 * 6)
= 5 * 24
= 120

看到了吗?先一层层往下递,触底后一层层往上归。

这就是「递归」这个名字的由来。

我曾经踩过的坑:有一次写递归,忘记写终止条件了。程序跑起来直接卡死,控制台疯狂报错「RecursionError: maximum recursion depth exceeded」。嗯,栈溢出了。

Python 默认递归深度是 1000 层。超过就报错。所以递归不是万能的,深度太大时,还是用循环吧。

1.4 递归 vs 循环

对比项 递归 循环
代码可读性 简洁,符合数学思维 需要手动维护状态
性能 有函数调用开销,可能栈溢出 通常更快
适用场景 树、图、分治算法 线性遍历、累加
理解难度 初学者容易晕 直观

说白了,递归和循环是等价的。任何递归都能改写成循环,反之亦然。

但有些问题,用递归写起来就是舒服。比如后面要讲的二叉树遍历、汉诺塔、快速排序——用循环写,代码能绕晕你。

1.5 本章知识体系

下面这张图,是我手绘的递归核心逻辑。你看一眼,就能明白递归到底在干什么。

递归核心逻辑 大问题 能直接解决? 是 直接返回结果 否 拆成更小的子问题 递归调用自身,直到满足终止条件

这张图你看懂了吗?

核心就两点:

  • 终止条件:问题小到可以直接解决
  • 递归步骤:把大问题拆成小问题,调用自身去解决

我刚开始学递归时,总想跟踪每一层的变量值。后来发现,这完全是自找麻烦。

你只需要相信:递归函数能正确处理比当前更小的问题。剩下的,交给计算机去栈里折腾吧。

一句话总结:递归就是「用自己定义自己」。数学归纳法教会我们:只要基础成立,递推成立,结论就成立。递归也一样:只要终止条件正确,递归步骤正确,结果就正确。

下一节,我们会深入递归的底层——栈帧。看看计算机到底是怎么一层层调用、一层层返回的。到时候你会恍然大悟:哦,原来递归是这样工作的!

可可精品资料库