二叉树递归遍历:先根与后根,递归思维的最佳试炼场

昨天我们把函数递归的基础打扎实了------阶乘、求和、斐波那契、汉诺塔,一路推演过来。今天进入递归最经典的应用场景:二叉树的遍历。我们把「普通递归」和「树递归」放到一起对比,帮你把中间那条线连起来,彻底搞醒豁。


零、二叉树是什么?------ 三分钟速通数据结构

在讲遍历之前,先把「二叉树」这个数据结构搞清楚。第一次接触「二叉树」?正常,咱们从最简单的概念开始。

直观理解

二叉树就像一棵倒挂的树------根在上,叶子在下。每个节点最多有两个分叉,左边那个叫左子树,右边那个叫右子树。

复制代码
        A          ← 根节点(Root),整棵树的起点
       / \
      B   C        ← B 是 A 的左子节点,C 是 A 的右子节点
     / \   \
    D   E   F      ← D、E、F 是叶子节点(下面没有节点了)

几个基本名词先记到:

  • 根节点:最顶上的那个,整棵树只有唯一一个根
  • 子节点:某个节点下面连着的节点(B、C 是 A 的子节点)
  • 叶子节点:下面没有子节点的节点(D、E、F 就是叶子)
  • 父节点:上面连着的那个(A 是 B、C 的父节点)

用 C 语言定义节点

c 复制代码
typedef struct TreeNode
{
    int data;                  // 节点存的数据
    struct TreeNode* left;     // 指向左子节点的指针
    struct TreeNode* right;    // 指向右子节点的指针
} TreeNode;

一个节点就三样东西:自己的数据左孩子在哪右孩子在哪 。而每个子树本身又是一个二叉树------这就是递归结构的本质:树由子树组成,子树也是树。

⚠️ 提醒struct TreeNode* left 里必须写 struct,因为 typedef 还没生效,编译器不认识 TreeNode。初学这里经常报错,先记住就行。


一、「普通递归」和「树递归」有啥不一样?

在上篇文章里,我们学的递归都是这样的:

c 复制代码
// 阶乘递归:每次只调一次自己
int fact(int n) {
    if (n == 0) return 1;       // 出口
    return n * fact(n - 1);     // 只调一次 fact
}

每次调用只会递归一次,函数调用链是一条直线往下走。

但树的递归不一样------一个节点有两个分支 ,所以一次函数调用里要递归两次

c 复制代码
void preorder(TreeNode* root) {
    if (root == NULL) return;
    printf("%d ", root->data);
    preorder(root->left);    // 第一次递归:走左子树
    preorder(root->right);   // 第二次递归:走右子树
}

这就产生了质变------普通递归是 「一条线」 ,树递归是 「一张网」

对比维度 普通递归(阶乘/求和/Fib) 树递归(遍历)
每次调几次自己 1 次 2 次(左 + 右)
出口条件 具体数值(n0, n1) 指针为空(root==NULL)
调用链形状 一条直线 树状分叉
典型问题 阶乘、Fib、汉诺塔 遍历、查找、计算深度
思维难度 ⭐⭐ ⭐⭐⭐⭐

💡 关键认知:树递归里的每个节点,都要把「自己做的事」做完、再把「左子树的事」和「右子树的事」也做完,才能返回。这就是为什么树递归的调用栈比普通递归复杂得多------后面手写推演的时候你会看到。


二、二叉树的三种遍历方式

遍历二叉树,就是按照某种规则「访问」树中的每一个节点。根据先处理根还是后处理根,分为三种:

遍历方式 英文名 访问顺序 一句话记法
先根遍历 Preorder → 左 → 右 进门先打卡
中根遍历 Inorder 左 → → 右 逛完左边再打卡
后根遍历 Postorder 左 → 右 → 全部逛完才打卡

本篇聚焦先根遍历后根遍历,这两者恰好形成鲜明对比。搞懂它们,中根自然也就通了。

为什么递归天然适合遍历树?

答案很简单:二叉树本身就是递归定义的------

复制代码
二叉树 = 根节点 + 左子树(也是一棵二叉树) + 右子树(也是一棵二叉树)

遍历一棵树 = 访问根 + 遍历左子树 + 遍历右子树。而「遍历左子树」和「遍历右子树」又是同样的问题,只是规模更小------完美匹配上篇文章讲的递归两个条件(出口 + 递推公式)。


三、先根遍历(Preorder Traversal)

规则

访问根节点 → 递归遍历左子树 → 递归遍历右子树

用一句话记住:「到自己家了,先打个卡,再去左右邻居家串门。」

代码实现

c 复制代码
void preorder(TreeNode* root)
{
    if (root == NULL)              // 递归出口:空树,直接返回
    {
        return;
    }
    printf("%d ", root->data);     // ① 先访问根节点
    preorder(root->left);          // ② 递归遍历左子树
    preorder(root->right);         // ③ 递归遍历右子树
}

三行核心逻辑,极其简洁。

手动推演执行过程

以这棵树为例:

复制代码
        1
       / \
      2   3
     / \   \
    4   5   6

执行 preorder(节点1)

复制代码
preorder(1)
  root != NULL,打印 1                          → 输出: "1 "
  调用 preorder(2)
    preorder(2)
      root != NULL,打印 2                      → 输出: "1 2 "
      调用 preorder(4)
        preorder(4)
          root != NULL,打印 4                  → 输出: "1 2 4 "
          调用 preorder(NULL) → 直接返回
          调用 preorder(NULL) → 直接返回
        返回
      调用 preorder(5)
        preorder(5)
          root != NULL,打印 5                  → 输出: "1 2 4 5 "
          调用 preorder(NULL) → 直接返回
          调用 preorder(NULL) → 直接返回
        返回
      返回
  调用 preorder(3)
    preorder(3)
      root != NULL,打印 3                      → 输出: "1 2 4 5 3 "
      调用 preorder(NULL) → 直接返回
      调用 preorder(6)
        preorder(6)
          root != NULL,打印 6                  → 输出: "1 2 4 5 3 6 "
          调用 preorder(NULL) → 直接返回
          调用 preorder(NULL) → 直接返回
        返回
      返回
  返回

最终输出:1 2 4 5 3 6

先根遍历的特征

  • 根节点总是第一个被访问
  • 遍历顺序反映的是「深度优先、根优先」的逻辑
  • 应用场景:复制一棵树(先建根节点再递归建子树)、序列化树结构、输出目录树

四、后根遍历(Postorder Traversal)

规则

递归遍历左子树 → 递归遍历右子树 → 访问根节点

用一句话记住:「先去左右邻居家串门,串完了回自己家再打卡。」

代码实现

c 复制代码
void postorder(TreeNode* root)
{
    if (root == NULL)              // 递归出口:空树,直接返回
    {
        return;
    }
    postorder(root->left);         // ① 递归遍历左子树
    postorder(root->right);        // ② 递归遍历右子树
    printf("%d ", root->data);     // ③ 最后访问根节点
}

和先根遍历相比,仅仅是 printf 的位置从第一行移到了最后一行。就这一行位置的变化,输出结果彻底不同。

手动推演执行过程

还是那棵树:

复制代码
        1
       / \
      2   3
     / \   \
    4   5   6

执行 postorder(节点1)

复制代码
postorder(1)
  调用 postorder(2)
    postorder(2)
      调用 postorder(4)
        postorder(4)
          调用 postorder(NULL) → 直接返回
          调用 postorder(NULL) → 直接返回
          打印 4                                 → 输出: "4 "
        返回
      调用 postorder(5)
        postorder(5)
          调用 postorder(NULL) → 直接返回
          调用 postorder(NULL) → 直接返回
          打印 5                                 → 输出: "4 5 "
        返回
      打印 2                                     → 输出: "4 5 2 "
    返回
  调用 postorder(3)
    postorder(3)
      调用 postorder(NULL) → 直接返回
      调用 postorder(6)
        postorder(6)
          调用 postorder(NULL) → 直接返回
          调用 postorder(NULL) → 直接返回
          打印 6                                 → 输出: "4 5 2 6 "
        返回
      打印 3                                     → 输出: "4 5 2 6 3 "
    返回
  打印 1                                         → 输出: "4 5 2 6 3 1 "

最终输出:4 5 2 6 3 1

后根遍历的特征

  • 根节点总是最后一个被访问
  • 必须先处理完所有子节点,才能处理父节点
  • 应用场景:删除一棵树(必须先删子节点再删父节点,否则子节点就丢了)、计算目录大小(先算子目录再汇总)

五、先根 vs 后根:一行之差,天壤之别

把两段代码放在一起对比:

c 复制代码
// 先根遍历:根 → 左 → 右
void preorder(TreeNode* root)
{
    if (root == NULL) return;
    printf("%d ", root->data);     // ← 根在这里,第一个执行
    preorder(root->left);
    preorder(root->right);
}

// 后根遍历:左 → 右 → 根
void postorder(TreeNode* root)
{
    if (root == NULL) return;
    postorder(root->left);
    postorder(root->right);
    printf("%d ", root->data);     // ← 根在这里,最后一个执行
}

同一棵树 [1,2,3,4,5,null,6] 的输出对比:

遍历方式 输出结果 根节点位置
先根遍历 1 2 4 5 3 6 第一个
后根遍历 4 5 2 6 3 1 最后一个

这个对比完美体现了上一篇文章的结论:递归语句前面的代码在递推阶段执行,后面的代码在回归阶段执行。 先根遍历在递推阶段就打印,后根遍历要等回归阶段才打印。

⚠️ 提醒 :很多兄弟伙会搞混「中根」和「后根」。记一个口诀:「先根是进门打卡,中根是左边看完打卡,后根是走完两边再打卡。」或者更粗暴------先根看代码顺序(printf → left → right),后根把 printf 丢最后。


六、完整可运行代码

为了方便你动手验证,这里给出一份完整的代码------手动构建一棵树并分别执行先根和后根遍历:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct TreeNode
{
    int data;
    struct TreeNode* left;
    struct TreeNode* right;
} TreeNode;

// 创建一个新节点
TreeNode* createNode(int data)
{
    TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
    node->data = data;
    node->left = NULL;
    node->right = NULL;
    return node;
}

// 先根遍历
void preorder(TreeNode* root)
{
    if (root == NULL) return;
    printf("%d ", root->data);
    preorder(root->left);
    preorder(root->right);
}

// 后根遍历
void postorder(TreeNode* root)
{
    if (root == NULL) return;
    postorder(root->left);
    postorder(root->right);
    printf("%d ", root->data);
}

int main()
{
    // 手动构建树:
    //        1
    //       / \
    //      2   3
    //     / \   \
    //    4   5   6
    TreeNode* root = createNode(1);
    root->left = createNode(2);
    root->right = createNode(3);
    root->left->left = createNode(4);
    root->left->right = createNode(5);
    root->right->right = createNode(6);

    printf("先根遍历: ");
    preorder(root);
    printf("\n");

    printf("后根遍历: ");
    postorder(root);
    printf("\n");

    return 0;
}

运行结果:

复制代码
先根遍历: 1 2 4 5 3 6
后根遍历: 4 5 2 6 3 1

🎓 面试技巧 :如果你能手写这段代码并且把执行过程讲清楚,基本上递归遍历这块就能过面试关了。面试官经常追问:「如果把 printf 换到中间,输出是什么?」------答案就是中根遍历 4 2 5 1 3 6


七、为什么递归遍历不会无限循环?

可能有兄弟伙会问:递归会调用自己,树的遍历也在不停调用 preorder,凭什么不会死循环?

答案藏在递归出口里:

c 复制代码
if (root == NULL) return;

树的每一个分支最终都会走到叶子节点。叶子节点的 leftright 都是 NULL。递归走到 NULL 时直接 return,不会再往下调------这就是触底反弹

递归深度 = 树的高度,而不是节点数量。 只要树的高度可控(一般不会超过几十层),就不会栈溢出。


八、从递归遍历到算法思维

二叉树递归遍历是理解很多高级算法的基础:

  • DFS(深度优先搜索) 本质上就是先根遍历的泛化
  • 回溯算法 的核心结构就是「递推 → 处理 → 回归」,和后根遍历的执行流程一致
  • 动态规划 的「自顶向下 + 记忆化」也可以类比为树的递归遍历

所以说,搞懂二叉树的递归遍历,就等于打开了算法世界的大门。 这是从「写代码」到「写算法」的关键一步。


九、经典面试题精讲 🎯

二叉树递归遍历是校招面试的高频考点,下面三道题,手写代码的概率极高。

面试题一:求二叉树的最大深度

题目描述:给定一棵二叉树,求它的最大深度(从根节点到最远叶子节点的路径上的节点数)。

LeetCode 104,几乎所有厂的面试都出过。

思路分析

树的深度 = max(左子树深度, 右子树深度) + 1(根自己算一层)。这本身就是个递推公式。

c 复制代码
int maxDepth(TreeNode* root)
{
    if (root == NULL)              // 出口:空树深度为 0
        return 0;
    int leftDepth = maxDepth(root->left);   // 左子树深度
    int rightDepth = maxDepth(root->right); // 右子树深度
    return (leftDepth > rightDepth ? leftDepth : rightDepth) + 1; // 取大的 +1
}

以我们那棵树 [1,2,3,4,5,null,6] 为例:

复制代码
maxDepth(1)
  left = maxDepth(2)
           left = maxDepth(4) → 1
           right = maxDepth(5) → 1
           return max(1,1)+1 = 2
  right = maxDepth(3)
            left = maxDepth(NULL) → 0
            right = maxDepth(6) → 1
            return max(0,1)+1 = 2
  return max(2,2)+1 = 3

最大深度 = 3。

🎓 面试技巧:这道题本质就是后根遍历------先算子节点深度,再汇总到父节点。掌握后根遍历,这一题就能秒。


面试题二:判断两棵二叉树是否相同

题目描述 :给定两棵二叉树的根节点 pq,判断它们是否完全相同(结构相同且每个节点的值相同)。

LeetCode 100,经典到不能再经典。

思路分析

两棵树相同,当且仅当:当前节点值相等 + 左子树相同 + 右子树相同

c 复制代码
bool isSameTree(TreeNode* p, TreeNode* q)
{
    // 出口 1:两个都为空 → 相同
    if (p == NULL && q == NULL)
        return true;
    // 出口 2:一个空一个不空 → 不相同
    if (p == NULL || q == NULL)
        return false;
    // 出口 3:当前节点值不同 → 不相同
    if (p->data != q->data)
        return false;
    // 递推:左右子树都相同才行
    return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}

关键点 :这里的递归是同时遍历两棵树,同步比较。注意三个出口条件缺一不可------这是面试官最爱挑刺的地方。


面试题三:根据遍历结果重建二叉树

题目描述:给定一棵二叉树的先根遍历序列和中根遍历序列(数组形式),要求重建这棵二叉树。

LeetCode 105,字节、阿里高频原题。

示例

  • 先根:[3, 9, 20, 15, 7]
  • 中根:[9, 3, 15, 20, 7]

重建结果:

复制代码
    3
   / \
  9  20
     / \
    15  7

思路分析

  1. 先根遍历的第一个数一定是根节点(这是先根的核心性质)
  2. 在中根遍历中找到这个根节点的位置,左边全是左子树,右边全是右子树(这是中根的核心性质)
  3. 对左右子树分别递归执行同样的操作
c 复制代码
// 假设 preorder[] 是先根序列, inorder[] 是中根序列
// preLeft/preRight: 先根序列的左右边界
// inLeft/inRight: 中根序列的左右边界
TreeNode* buildTree(int* preorder, int preLeft, int preRight,
                    int* inorder,  int inLeft,  int inRight)
{
    if (preLeft > preRight)        // 出口:空区间
        return NULL;

    int rootVal = preorder[preLeft];   // 先根第一个就是根
    TreeNode* root = createNode(rootVal);

    // 在中根序列里找根的位置
    int rootIndex = inLeft;
    while (inorder[rootIndex] != rootVal)
        rootIndex++;

    int leftSize = rootIndex - inLeft;  // 左子树节点个数

    // 递归构建左右子树
    root->left  = buildTree(preorder, preLeft + 1, preLeft + leftSize,
                            inorder,  inLeft, rootIndex - 1);
    root->right = buildTree(preorder, preLeft + leftSize + 1, preRight,
                            inorder,  rootIndex + 1, inRight);
    return root;
}

🎓 面试技巧 :这道题的突破口就一句话------「先根给根,中根分左右」。面试官问「为什么必须知道两种遍历才能重建」,标准答案是:只用先根无法区分左右子树,只用中根无法确定根。先根 + 中根组合才能唯一确定一棵二叉树。


📝 本节知识点速查

知识点 一句话记住
二叉树节点结构 data + left 指针 + right 指针
普通递归 vs 树递归 普通递归每次调自己 1 次(一条线),树递归调 2 次(一张网)
先根遍历(Preorder) 根 → 左 → 右,printf 在最前面
后根遍历(Postorder) 左 → 右 → 根,printf 在最后面
中根遍历(Inorder) 左 → 根 → 右,printf 在中间
递归出口 root == NULLreturn,叶子节点的左右孩子触发
先根应用 树的复制与序列化
后根应用 树的删除(先删子节点再删父节点)、计算深度
最大深度 后根遍历:max(左深度, 右深度) + 1
判断相同树 同时遍历两棵树:值相同 + 左相同 + 右相同
重建二叉树 「先根给根,中根分左右」,先根 + 中根唯一确定树
递归深度 等于树的高度,不是节点数量
核心差异 先根在递推阶段打印,后根在回归阶段打印

结语

从昨天的「一条线递归」(阶乘、Fib),到今天的「一张网递归」(二叉树遍历),你其实已经完成了递归思维里最关键的一次跃迁。

普通递归 → 树递归,就是把「一条路走到黑」升级成「分岔路口每条路都走一遍」。 这个思维一旦形成,后面学 DFS、回溯、分治,全部是顺水推舟。

把先根和后根的推演过程自己手写一遍,三道面试题的代码亲手敲一遍------递归在你手里就不是「玄学」而是实打实的工具了。


相关推荐
Gauss松鼠会2 小时前
【GaussDB】GaussDB锁阻塞源头查询
java·开发语言·前端·数据库·算法·gaussdb·经验总结
oier_Asad.Chen2 小时前
【洛谷题解/AcWing题解】洛谷P4011 孤岛营救问题/AcWing1131拯救大兵瑞恩
数据结构·笔记·学习·算法·动态规划·图论·宽度优先
mingo_敏2 小时前
DeepAgents : 后端(Backends)
java·开发语言
mabing9932 小时前
Qt QMessageBox、QDialogButtonBox中英文翻译动态切换
开发语言·qt
Sagittarius_A*2 小时前
【RCELABS】Level 17~18 —— PHP命令执行函数与环境变量注入
开发语言·安全·web安全·靶场·php·rce
冻柠檬飞冰走茶2 小时前
PTA基础编程题目集 7-19 支票面额(C语言实现)
c语言·开发语言·数据结构·算法
船漏了就会沉2 小时前
状态压缩DP:小数据下的“暴力美学”
算法
破碎的南瓜3 小时前
靶场中使用到的php函数以及每种漏洞的防御方式
开发语言·php
你怎么知道我是队长3 小时前
计算机虚拟存储管理与页面置换算法详解
服务器·网络·算法
罗西的思考3 小时前
【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 — (14)— Teacher
人工智能·笔记·深度学习·算法·机器学习