递归(recursion)指的是:一个函数在自己的函数体里调用了自己。递归能写出非常简洁、读起来很顺的代码,但要是没写好,也会把内存吃光。
什么时候会用到递归
现实中递归最常见的舞台:
- 遍历递归型的数据结构,链表、二叉树这类由自己组成自己的玩意儿。
- 游戏里探索所有可能走法(比如棋类)时自然形成树形递归。
递归的两个部分
任何能正确结束的递归,都必然由两块拼成:
- 终止条件(terminating case),递归停下来、直接返回的出口。
- 递归调用(recursive step),调用自己,且必须朝着终止条件推进。
缺了终止条件,或者递归不朝终止条件走,就会无限套娃,直到把栈耗尽崩溃。
例子:用递归实现乘法
下面这个函数用"递归地把 y 累加 x 次"实现乘法,先看这个递归示例:
#include <stdio.h>
unsigned int multiply(unsigned int x, unsigned int y) {
if (x == 1) {
/* 终止条件:x 已经减到 1,直接返回 y */
return y;
} else if (x > 1) {
/* 递归步:y + 再把 x-1 和 y 相乘 */
return y + multiply(x - 1, y);
}
/* 兜住 x 为 0 的情况 */
return 0;
}
int main() {
printf("3 乘 5 等于 %d\n", multiply(3, 5)); // 15
return 0;
}脑内推演一遍 multiply(3, 5):
multiply(3, 5)
= 5 + multiply(2, 5)
= 5 + (5 + multiply(1, 5))
= 5 + (5 + 5) ← 触到终止条件 multiply(1,5)=5
= 15注意每一步的 x - 1,它在朝终止条件 x == 1 一步步接近。要是写成 multiply(x, y) 就永远刹不住车了。
经典练习:递归求阶乘
阶乘定义为 n! = n × (n-1) × … × 2 × 1,规定 0! = 1。用递归写特别顺手:
#include <stdio.h>
/* 声明 */
unsigned int factorial(unsigned int n);
int main() {
printf("0! = %u\n", factorial(0));
printf("1! = %u\n", factorial(1));
printf("3! = %u\n", factorial(3));
printf("5! = %u\n", factorial(5));
return 0;
}
/* 定义 */
unsigned int factorial(unsigned int n) {
if (n <= 1) {
return 1; /* 终止条件:0! = 1! = 1 */
}
return n * factorial(n - 1); /* 递归步:n! = n × (n-1)! */
}输出:
0! = 1
1! = 1
3! = 6
5! = 120每一步都在计算 n × factorial(n-1),让问题规模小一号,直到 n <= 1 上岸。
递归的代价:栈会变深
递归优雅,但有代价,每次调用都占一个栈帧。层数较深时内存疯涨,极端情况下直接把栈耗光(栈溢出)。和循环相比:
- 能干一样的事,代码通常更短、更好读(尤其对树、链表这类结构)。
- 但效率常不如循环,且深度有上限。能用迭代就迭代,遇到天然递归结构再用递归。
判断递归是否写对,就问自己三个问题:终止条件在哪?递归调用朝不朝终止条件走?每一步问题规模有没有变小?