什么是递归?一文讲透函数自己调用自己的原理与例子

编程狮(w3cschool.cn) 2026-08-11 14:32:08 浏览数 (21)
反馈

你刷算法题时遇到"用递归写斐波那契",照着答案抄能跑,但让自己写就卡壳:函数到底怎么调用自己?为什么不会无限循环?简单说,递归就是函数自己调用自己,而所谓递归函数,就是会在执行期间调用自身的一种函数,但它必须有一个明确的终止条件,否则就会一直调下去直到栈溢出。本文基于 Python 3.10 演示,把递归拆成"基线条件 + 递归步骤"两部分,配合阶乘、遍历文件夹等例子讲清它为什么能 work、又为什么会爆栈。今天这篇文章,编程狮就用大白话把它讲透。

一、递归一句话定义:函数自己调用自己

递归,就是一个函数在执行过程中又调用了它自己。所谓递归函数,必须满足两个前提:第一,每次调用都要让问题规模变小,朝着能直接解决的方向走;第二,必须有一个不再继续调用的出口,也就是基线条件。

最常见的一个比喻是"俄罗斯套娃":打开一个大娃娃,里面还是一个一模一样的小娃娃,再打开还是。函数也是这样,每次调用都进入一个更小规模、但逻辑相同的自己。理解了套娃这个画面,你就抓到了它的精髓:这不是"重复",而是"把同一个动作作用在越来越小的规模上",直到小到不用再分。

缺了出口,函数会一层层调用下去,直到把内存里的调用栈撑爆,抛出 RecursionError。这部分内存叫调用栈,每调用一次函数就压入一帧、保存当前参数,返回时再弹出,调用和返回一一对应。平时写代码,遇到"先把大问题拆成小问题、小到一定程度直接给答案"的模式,就可以想想能不能用这种写法。它不炫技,而是让代码更贴近问题本身的结构。初学者先接受"函数能调用自己"这件事,后面看代码会顺很多。

如果你刚学 Python,可以先过一遍 Python 基础入门教程,把函数和循环这两个基础打牢。

二、拆开看:递归 = 基线条件 + 递归步骤

几乎所有递归都能拆成两块。基线条件是"最小、不用再分的情况",直接返回答案;递归步骤是"把大问题拆成小问题,再调用自己"。以阶乘为例,n! 等于 n 乘以 (n-1)!:

def factorial(n):
    if n == 0:            # 基线条件:0! = 1
        return 1
    return n * factorial(n - 1)  # 递归步骤:交给更小的自己

当你调用 factorial(3),它会变成 3 factorial(2),再变成 3 2 * factorial(1),一直到 factorial(0) 命中基线返回 1,然后一层层把结果乘回来。每进入一层,调用栈就压入一帧保存当前 n;命中基线后,从最深那层开始返回、栈也随之弹出。这个过程就像叠盘子再一个个撤下来。更多编程范式相关的概念,软件工程入门教程 里有更系统的讲解。

写递归时,先在纸上画出前两层调用,往往比盯着代码空想更管用。

调用栈可以理解成一摞盘子:每调用一层,就往顶上放一个写着当前参数的盘子;函数返回时,再从顶上拿走最上面的那个。所以层数越深,盘子堆得越高,堆到超过上限就塌了——这就是栈溢出的直观画面。基线条件的作用,就是让这摞盘子及时停止往上堆。

三、三个经典例子:阶乘、斐波那契、遍历文件夹

阶乘之外,斐波那契是另一个经典。第 n 项等于前两项之和,基线条件是 n 为 0 或 1:

def fib(n):
    if n < 2:            # 基线条件
        return n
    return fib(n - 1) + fib(n - 2)  # 递归步骤

第三个例子更贴近实际:遍历一个文件夹下的所有子文件夹。目录结构是天然的树,递归刚好能一层层钻进去:

import os


def walk(folder):
    for name in os.listdir(folder):
        path = os.path.join(folder, name)
        if os.path.isdir(path):
            walk(path)   # 遇到子文件夹,递归进去
        else:
            print(path)  # 是文件就处理

其实很多树形结构都适合这么写。比如二叉树的前序遍历,就是"先访问根节点,再递归访问左子树、右子树"。为什么树形结构特别适合这种写法?因为树本身就是"一个节点挂着若干子节点"的定义,用循环去模拟反而要自己维护一个栈,代码又长又容易错。把"访问根、再处理子树"直接写成"先处理、再调用自身处理子树",和定义几乎一一对应,读起来也顺。递归思维的本质,是用"更小的自己"去解决"更大的问题",只要拆得对,再复杂的结构也能被几行代码表达清楚。语法细节拿不准,在线编程词典 能直接查 os 模块和递归相关的条目。

四、递归和循环怎么互相改写,以及两个常见坑

递归和循环经常能互相改写。比如阶乘用循环写就是从头乘到尾,没有函数自己调自己,也就没有栈溢出的风险。一般来说,逻辑本身像"树"或"分治"的问题用递归更直观;简单的重复动作用循环更省内存。

两个最常见的坑:一是忘了写基线条件,或者基线条件永远命中不了,结果无限递归直到报错;二是重复计算,像上面的斐波那契,fib(n-1) 和 fib(n-2) 会反复算同一项,数据稍大就慢得离谱,这种要用"记忆化"或干脆改成循环。

还有一点值得知道:如果递归调用是函数里的最后一步(叫尾递归),有些语言会把它优化成循环、不占新栈帧;但 Python 不做这种优化,所以即便写成尾递归,层数深了一样会爆栈。这也是为什么数据量大时优先考虑循环。理解了这个边界,你就不会迷信递归,也不会害怕它——它就是工具箱里的一件趁手工具。

把阶乘从递归改成循环,其实就是把"交给更小的自己"换成"用变量一路乘下去":

def factorial_loop(n):
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

没有函数调用自己,也就没有栈帧堆积,n 再大也不会爆栈。遇到能用循环清晰表达的场景,优先选循环更稳。

下面把两种写法摆在一起对比:

写法 适合场景 优点 代价
递归 树形、分治类问题 代码直观、贴近定义 占调用栈、易重复计算
循环 线性重复动作 省内存、好控制 复杂结构写起来绕

一句话总结怎么选:问题本身像树、像分形,或者能自然拆成相同结构的子问题,用递归最省脑;问题是一条线、或者数据规模可能很大,用循环更靠谱。新手常犯的错是一上来就硬套递归,其实先把循环写对、再想能不能递归,会更稳。

总结

递归就是函数自己调用自己,一个合格的递归函数必须配一个明确的基线条件作为出口,否则会无限调用直到栈溢出。记住三句话:

  • 递归 = 基线条件 + 递归步骤,两者缺一不可;
  • 每次调用都要让问题规模变小,逼近基线;
  • 递归和循环可互转,重复计算多或数据大时优先考虑循环。

想继续夯实编程底层思维,可以顺着下面的路径走。

延伸学习

想把基础算法思维补扎实,可以按这个顺序来:

  1. 想系统学编程基础,编程基础学习路线 帮你规划进阶;
  2. 想动手练,编程实战课程 提供大量可上手的算法小项目。

常见问题

Q:递归为什么不会无限循环?
A:因为有基线条件。一旦参数小到命中基线,函数就不再调用自己,直接返回,从而终止整条调用链。

Q:递归和循环哪个更好?
A:没有绝对好坏。树形、分治类问题递归更直观;简单重复或数据量大时,循环更省内存、更可控。

Q:什么是栈溢出,递归为什么会引发?
A:每次函数调用都会在调用栈占一块空间,递归层数太深会把栈撑满,Python 会抛出 RecursionError,这就是栈溢出。

Q:斐波那契的递归为什么那么慢?
A:因为它大量重复计算同一项,比如 fib(5) 会反复算 fib(3)、fib(2)。用记忆化缓存或改成循环能显著提速。

0 人点赞