4. 二叉树的遍历(上):前序、中序、后序遍历(递归实现)

遍历二叉树,说白了就是按照某种规则,把树里的每个节点都访问一遍,而且不重复、不遗漏。这就像你走进一个迷宫,得有个策略才能确保每个房间都走到,不会迷路。

我个人习惯把遍历理解为“打印节点值的顺序”。你想想看,一棵二叉树长什么样?根节点在中间,左边挂着左子树,右边挂着右子树。那访问顺序的不同,就产生了三种经典的遍历方式:前序、中序、后序。

嗯,这里要注意,这三种遍历的名字,其实指的是“根节点”被访问的时机。

4.1 前序遍历(Preorder Traversal)

前序遍历的规则很简单:先访问根节点,再遍历左子树,最后遍历右子树。用一句话记就是“根左右”。

我在项目中遇到过这样一个场景:需要序列化一棵二叉树,把它存到文件里。当时我用的就是前序遍历。为什么?因为前序遍历的第一个节点就是根节点,反序列化的时候,拿到第一个值就能立刻创建根节点,非常方便。

来看代码实现:

// 前序遍历:根 -> 左 -> 右
void preorderTraversal(TreeNode* root) {
    if (root == NULL) {
        return;  // 空树,直接返回
    }
    
    // 1. 访问根节点
    printf("%d ", root->data);
    
    // 2. 递归遍历左子树
    preorderTraversal(root->left);
    
    // 3. 递归遍历右子树
    preorderTraversal(root->right);
}

你看,代码非常简洁。递归的魅力就在这里——你不需要关心左子树里面还有多少层,递归会帮你搞定一切。

小技巧: 如果你在调试时想快速验证前序遍历的结果,可以记住一个口诀:“根节点先打印,左子树走到底,再回头处理右子树”。

4.2 中序遍历(Inorder Traversal)

中序遍历的规则是:先遍历左子树,再访问根节点,最后遍历右子树。口诀是“左根右”。

中序遍历有一个非常重要的特性:对于二叉搜索树(BST),中序遍历的结果是递增有序的。我曾经用这个特性来验证一棵树是不是合法的二叉搜索树——只要中序遍历的结果是严格递增的,那就没问题。

代码实现如下:

// 中序遍历:左 -> 根 -> 右
void inorderTraversal(TreeNode* root) {
    if (root == NULL) {
        return;
    }
    
    // 1. 递归遍历左子树
    inorderTraversal(root->left);
    
    // 2. 访问根节点
    printf("%d ", root->data);
    
    // 3. 递归遍历右子树
    inorderTraversal(root->right);
}

你发现没有?前序和中序的代码几乎一模一样,唯一的区别就是 printf 的位置不同。前序放在最前面,中序放在中间。这就是递归遍历的规律——打印语句的位置决定了遍历顺序

避坑指南: 我曾经在写中序遍历时,不小心把递归调用写成了 inorderTraversal(root) 而不是 inorderTraversal(root->left)。结果程序直接栈溢出了。递归调用一定要传子节点,千万别传自己!

4.3 后序遍历(Postorder Traversal)

后序遍历的规则是:先遍历左子树,再遍历右子树,最后访问根节点。口诀是“左右根”。

后序遍历在实际开发中有一个经典用途:删除整棵树。你想啊,要删除一棵树,你得先把子节点都删掉,最后才能删根节点。这不就是后序遍历的思路吗?

代码实现:

// 后序遍历:左 -> 右 -> 根
void postorderTraversal(TreeNode* root) {
    if (root == NULL) {
        return;
    }
    
    // 1. 递归遍历左子树
    postorderTraversal(root->left);
    
    // 2. 递归遍历右子树
    postorderTraversal(root->right);
    
    // 3. 访问根节点
    printf("%d ", root->data);
}

后序遍历的代码,printf 放到了最后。你看,三种遍历的代码结构完全一样,就是 printf 的位置不同。这就是递归遍历的核心规律。

4.4 三种遍历的对比

为了让你看得更清楚,我把三种遍历放在一起对比:

遍历方式 访问顺序 口诀 典型应用
前序遍历 根 → 左 → 右 根左右 序列化二叉树、复制树
中序遍历 左 → 根 → 右 左根右 BST 排序输出
后序遍历 左 → 右 → 根 左右根 删除树、计算目录大小

举个例子,假设有这样一棵二叉树:

      1
     / \
    2   3
   / \
  4   5

三种遍历的结果分别是:

  • 前序: 1 2 4 5 3
  • 中序: 4 2 5 1 3
  • 后序: 4 5 2 3 1

你仔细看看中序的结果:4 2 5 1 3。根节点 1 在中间,左边是左子树的所有节点(4 2 5),右边是右子树的节点(3)。这就是“左根右”的体现。

4.5 递归遍历的核心规律

说了这么多,其实就一句话:递归遍历的代码结构是固定的,改变 printf 的位置就能改变遍历顺序

我建议你在学习时,不要死记硬背代码,而是理解递归的调用栈。每次递归调用,都会把当前函数的上下文压入栈中。当递归返回时,再从栈中弹出继续执行。这就是递归能“记住”走到哪里的原因。

核心要点:
  • 前序:先处理根,再处理左右子树
  • 中序:先处理左子树,再处理根,最后处理右子树
  • 后序:先处理左右子树,最后处理根
  • 三种遍历的时间复杂度都是 O(n),空间复杂度 O(h),h 为树的高度

最后,我用一张图来总结这三种遍历的流程。你可以把它当作一个“决策树”,帮助你快速判断该用哪种遍历。

二叉树递归遍历决策图 遍历开始 前序:根左右 中序:左根右 后序:左右根 1. 访问根节点 2. 递归左子树 3. 递归右子树 1. 递归左子树 2. 访问根节点 3. 递归右子树 1. 递归左子树 2. 递归右子树 3. 访问根节点 口诀:前序根左右,中序左根右,后序左右根

嗯,递归遍历的内容就讲到这里。你只要记住:代码结构不变,printf 位置决定顺序。下次写代码的时候,先想清楚你要先处理根还是后处理根,然后直接把 printf 放在对应的位置就行。


公众号:蓝海资料掘金营,微信deep3321