循环是递归的特例,递归与栈等价。 所有循环可改写为尾递归;所有递归可显式用栈模拟为循环。理解这一等价性,是算法设计与编程内功的关键。
int sum_iter(int n) {
int s = 0;
for (int i = 1; i <= n; ++i) s += i;
return s;
}
int sum_rec(int n) {
if (n == 0) return 0;
return sum_rec(n - 1) + n;
}
s,从 1 累加到 n。return sum_rec(n-1, acc+n),编译器可消除栈帧,变成循环。// 循环
int find_iter(const int *a, int n, int v) {
for (int i = 0; i < n; ++i) if (a[i] == v) return i;
return -1;
}
// 递归(辅助函数隐藏索引)
int find_rec_helper(const int *a, int n, int v, int i) {
if (i == n) return -1;
if (a[i] == v) return i;
return find_rec_helper(a, n, v, i + 1);
}
int find_rec(const int *a, int n, int v) {
return find_rec_helper(a, n, v, 0);
}
递归版本引入额外参数 i 替代循环变量,本质相同。
struct Node { int val; Node *next; };
// 循环
void print_iter(Node *head) {
for (Node *p = head; p; p = p->next) printf("%d ", p->val);
}
// 递归
void print_rec(Node *head) {
if (!head) return;
printf("%d ", head->val);
print_rec(head->next);
}
递归极其简洁,但长链表会栈溢出。工程上单链表遍历强制用循环。
以二叉树后序遍历为例:
struct Frame { Node *node; int state; }; // state: 0=未访左, 1=未访右, 2=访问根
void postorder_iter(Node *root) {
Frame st[100]; int top = 0;
if (root) st[top++] = {root, 0};
while (top) {
Frame *f = &st[top-1];
if (f->state == 0) {
f->state = 1;
if (f->node->left) st[top++] = {f->node->left, 0};
} else if (f->state == 1) {
f->state = 2;
if (f->node->right) st[top++] = {f->node->right, 0};
} else {
printf("%d ", f->node->val);
--top;
}
}
}
每个栈帧保存“执行到哪一步”,完美模拟递归调用栈。
| 场景 | 推荐 | 理由 |
|---|---|---|
| 线性结构遍历(数组、链表) | 循环 | O(1) 空间、无栈溢出、编译器优化好 |
| 树/图遍历(深度未知、分支多) | 递归/显式栈 | 代码简洁、符合数学定义;深度大时改显式栈 |
| 分治算法(归并排序、快排、最近点对) | 递归 | 问题自然分解,递归表达最清晰 |
| 动态规划(自底向上) | 循环 | 填表顺序明确,避免重复计算 |
| 回溯/搜索(N皇后、迷宫、子集生成) | 递归 | 状态回溯天然匹配调用栈 |
// 非尾递归
int fact(int n) { return n == 0 ? 1 : n * fact(n-1); }
// 尾递归(累加器模式)
int fact_tail(int n, int acc) { return n == 0 ? acc : fact_tail(n-1, acc*n); }
int fact(int n) { return fact_tail(n, 1); }
GCC/Clang -O2 可将尾递归优化为跳转,栈帧常数。但 C++ 标准不保证,工程上不依赖 TCO,长递归请改循环/显式栈。
理解循环与递归的本质统一,不再被语法形式束缚,才能在算法设计中游刃有余。