CC++ & Algorithm

二叉树的遍历(前序、中序、后序、层序)

极难3
语言版本:通用
概述:用走迷宫和拍集体照的有趣比喻,学会四种方式遍历一棵二叉树,并给出递归和迭代两种实现代码。

走迷宫、拍照片、排座位——一次搞懂二叉树的四种遍历

你组织全班同学去参观一个神奇的“二叉树乐园”。乐园的每个景点(节点)最多只有两个岔路:左边和右边。你想把每个景点都打卡一遍,并且记录下打卡的顺序。你有四种不同的游览路线:

  • 前序:“先拍照再探险”——到了岔路口,先掏出手机拍一张景点照,然后钻进左边小路,走到底后再回头走右边。
  • 中序:“左边到底,回来拍照,再去右边”——先走进左边小路,一直走到不能再走,返回岔路口拍照,然后再走右边。
  • 后序:“先玩遍两边,最后拍照”——先把左边和右边的小路都走完,最后回到岔路口补拍一张。
  • 层序:“一层一层拍”——从入口开始,先拍完第一层的所有景点(从左到右),再下到第二层,以此类推。

这四种“游览路线”就是二叉树的四种遍历方式。遍历(Traversal)是指按照某种规则,访问树中的每一个节点恰好一次,从而把树这种“非线性”结构转换成一条“线性的”序列。不同遍历顺序得到的序列不同,适用于不同的场景。比如中序遍历二叉搜索树可以得到从小到大排序的序列;层序遍历常用于求树的宽度、最短路径等。

接下来,我们用下面这棵标准的二叉树来演示每种遍历。为了方便记忆,节点用大写字母标记。

        A
      /   \
     B     C
    / \   / \
   D   E F   G

一、四种遍历的原理与过程

1. 前序遍历(Preorder):根 → 左 → 右

顺序:先访问当前节点(根),然后递归遍历左子树,最后递归遍历右子树。

过程详解(像不像“先拍照再向左走”?):

  1. 站在节点 A → 访问 A(拍照)
  2. 进入左子树(B 为根)→ 访问 B → 进入左子树(D 为根)→ 访问 D → D 的左右孩子都是空,返回 B
  3. 进入 B 的右子树(E 为根)→ 访问 E → 返回 B → 返回 A
  4. 进入右子树(C 为根)→ 访问 C → 进入左子树(F)→ 访问 F → 进入右子树(G)→ 访问 G

结果序列:A B D E C F G

生活例子:你走进一个迷宫,每到一个分岔口就立刻记录当前房间的名字,然后先探索左边的所有房间,再探索右边的。这样,每条路第一次遇到时就会被记下。

2. 中序遍历(Inorder):左 → 根 → 右

顺序:先递归遍历左子树,然后访问当前节点,最后递归遍历右子树。

过程详解(“先向左走到尽头,再回头拍中间,最后向右”):

  1. 从根 A 开始,先向左,进入子树 B → 再向左,进入子树 D → D 左空,访问 D(拍照)→ D 右空,返回 B
  2. 访问 B → 进入 B 的右子树 E → E 左空,访问 E → E 右空,返回 B → 返回 A
  3. 访问 A → 进入右子树 C → 向左进入 F → F 左空,访问 F → F 右空,返回 C
  4. 访问 C → 进入 C 的右子树 G → G 左空,访问 G → G 右空

结果序列:D B E A F C G

生活例子:你在一棵二叉树形状的书架上找书,每到一个书架就先查看左边抽屉里的书,再打开中间抽屉拍照,最后查看右边抽屉。这样拿到的书是按照“左-中-右”顺序排好的。

3. 后序遍历(Postorder):左 → 右 → 根

顺序:先递归遍历左子树,然后递归遍历右子树,最后访问当前节点。

过程详解(“左右全逛完,最后回来拍照”):

  1. 从 A 开始,向左进入 B → 向左进入 D → D 左右空,访问 D → 返回 B
  2. 进入 B 的右子树 E → E 左右空,访问 E → 返回 B → 访问 B → 返回 A
  3. 进入右子树 C → 向左进入 F → F 左右空,访问 F → 返回 C
  4. 进入 C 的右子树 G → G 左右空,访问 G → 返回 C → 访问 C → 返回 A → 访问 A

结果序列:D E B F G C A

生活例子:你清理房间,先打扫左房间的每个角落(全部完成),再打扫右房间,最后回到客厅拍一张完工照。这样“根”节点是最后一个被访问的。

4. 层序遍历(Level-order / BFS):从上到下,从左到右

顺序:按层从上到下,每层内从左到右依次访问。

过程详解(“就像排队报数,按层叫号”):

  1. 队列初始化,根 A 入队
  2. 出队 A → 访问 A → 左孩子 B 入队 → 右孩子 C 入队
  3. 出队 B → 访问 B → 左孩子 D 入队 → 右孩子 E 入队
  4. 出队 C → 访问 C → 左孩子 F 入队 → 右孩子 G 入队
  5. 出队 D → 访问 D → 无孩子
  6. 出队 E → 访问 E
  7. 出队 F → 访问 F
  8. 出队 G → 访问 G

结果序列:A B C D E F G

生活例子:班里的同学按身高站成二叉树形状,老师从第一排最左边开始点名字,然后第二排从左到右……一排一排地记录。


二、递归实现(C++ 和 Python)——代码已存在,这里补充理解要点

其实你已经在前面的 C++ 和 Python 代码中看到了递归版本。我们再来强调几个关键点:

  • 递归出口if (root == nullptr) return;(C++)/ if root is None: return(Python)。千万别漏掉这行,否则程序会无限递归直到栈溢出崩溃!
  • “访问”节点的位置:打印输出语句的位置决定了顺序。前序把输出放在递归前后;中序放在左递归之后、右递归之前;后序放在两个递归之后。
  • 递归栈:递归其实背后用到了系统栈(调用栈),每递归一层像压一个栈帧。树很深时(例如 10000 层),栈可能爆掉(栈溢出)。这时可以考虑下面要讲的迭代版本。

记忆小口诀(很多同学靠这个记住顺序):

  • 前序:左右
  • 中序:左
  • 后序:左右
  • 层序:用队列,一层一层扫

三、迭代实现(用栈代替递归,避免栈溢出)

当二叉树深度很大(比如有 1 万层),递归会导致栈溢出。可以用显式栈来模拟递归过程。下面给出前序、中序、后序的迭代版本(层序本身就用队列,已经是迭代)。

3.1 前序遍历迭代(用栈,先压右孩子再压左孩子)

思路:访问根节点,然后先压右孩子、再压左孩子(因为栈是后进先出,保证左孩子先被处理)。

// C++ 前序迭代版本
#include <stack>
void preorderIterative(TreeNode* root) {
    if (root == nullptr) return;
    stack<TreeNode*> st;
    st.push(root);
    while (!st.empty()) {
        TreeNode* node = st.top();
        st.pop();
        cout << node->data << " ";          // 访问当前节点
        // 先压右孩子,再压左孩子(这样左孩子先出栈)
        if (node->right) st.push(node->right);
        if (node->left) st.push(node->left);
    }
}
# Python 前序迭代版本
def preorder_iterative(root):
    if root is None:
        return
    stack = [root]
    while stack:
        node = stack.pop()
        print(node.data, end=' ')
        # 先压右,再压左,保证左先出
        if node.right:
            stack.append(node.right)
        if node.left:
            stack.append(node.left)

3.2 中序遍历迭代(用栈模拟“向左走到尽头”)

思路:从根开始,一直向左走,把沿途节点压入栈,直到左为空。然后出栈一个节点,访问它,再处理它的右子树。

void inorderIterative(TreeNode* root) {
    stack<TreeNode*> st;
    TreeNode* curr = root;
    while (curr != nullptr || !st.empty()) {
        // 一直向左,把所有左节点压栈
        while (curr != nullptr) {
            st.push(curr);
            curr = curr->left;
        }
        // 出栈,访问节点
        curr = st.top();
        st.pop();
        cout << curr->data << " ";
        // 转向右子树
        curr = curr->right;
    }
}
def inorder_iterative(root):
    stack = []
    curr = root
    while curr or stack:
        # 一路向左压栈
        while curr:
            stack.append(curr)
            curr = curr.left
        # 出栈访问
        curr = stack.pop()
        print(curr.data, end=' ')
        # 转向右
        curr = curr.right

3.3 后序遍历迭代(稍复杂,用两个栈或一个栈加标记)

经典方法:用两个栈。第一个栈按“根->右->左”入栈,第二个栈保存访问顺序,最后逆序输出。

void postorderIterative(TreeNode* root) {
    if (root == nullptr) return;
    stack<TreeNode*> st1, st2;
    st1.push(root);
    while (!st1.empty()) {
        TreeNode* node = st1.top();
        st1.pop();
        st2.push(node);                // 把访问顺序暂存在 st2
        if (node->left) st1.push(node->left);
        if (node->right) st1.push(node->right);
    }
    // 从 st2 依次出栈得到后序序列
    while (!st2.empty()) {
        cout << st2.top()->data << " ";
        st2.pop();
    }
}
def postorder_iterative(root):
    if root is None:
        return
    st1 = [root]
    st2 = []
    while st1:
        node = st1.pop()
        st2.append(node)
        if node.left:
            st1.append(node.left)
        if node.right:
            st1.append(node.right)
    while st2:
        node = st2.pop()
        print(node.data, end=' ')

四、新手常犯的错误(附解决方法)

  1. 忘记递归出口
    错误:void preorder(TreeNode* root) { cout << root->data; ... } 没检查空指针。当树为空或递归到叶子节点的孩子时,程序崩溃。
    解决:始终在函数最前面写 if (root == nullptr) return;

  2. 层序遍历时忘记检查左右孩子是否为空
    错误:q.push(current->left); 如果左孩子是 nullptr,会入队空指针,可能引发后续访问错误。
    解决:加判断 if (current->left) q.push(current->left);

  3. 混淆三种递归顺序
    症状:把中序遍历写成了前序,导致序列不对。
    解决:用口诀“根左右、左根右、左右根”对照代码位置。

  4. 迭代版栈的顺序搞反
    前序迭代:先压右再压左(左先出)。
    中序迭代:记得先一路向左压栈。
    后序迭代:用两栈法比较稳妥。

  5. 忘记层序使用队列
    有人试图用栈实现层序,结果走出“螺旋”顺序。层序必须用队列(先进先出)。


五、完整可运行代码示例(带 main 和测试)

因为你的原始内容已经给了完整的 C++ 和 Python 代码(递归+层序),这里我补充一个包含递归和迭代所有四种遍历的完整 C++ 程序,并加上注释。Python 类似,不重复。

#include <iostream>
#include <queue>
#include <stack>
using namespace std;

// 二叉树节点结构体
struct TreeNode {
    char data;              // 节点数据(用字符方便显示)
    TreeNode *left;         // 左孩子指针
    TreeNode *right;        // 右孩子指针
    TreeNode(char val) : data(val), left(nullptr), right(nullptr) {}
};

// ---------- 递归遍历 ----------
void preorderRecursive(TreeNode* root) {
    if (root == nullptr) return;
    cout << root->data << " ";
    preorderRecursive(root->left);
    preorderRecursive(root->right);
}

void inorderRecursive(TreeNode* root) {
    if (root == nullptr) return;
    inorderRecursive(root->left);
    cout << root->data << " ";
    inorderRecursive(root->right);
}

void postorderRecursive(TreeNode* root) {
    if (root == nullptr) return;
    postorderRecursive(root->left);
    postorderRecursive(root->right);
    cout << root->data << " ";
}

// ---------- 迭代遍历 ----------
void preorderIterative(TreeNode* root) {
    if (root == nullptr) return;
    stack<TreeNode*> st;
    st.push(root);
    while (!st.empty()) {
        TreeNode* node = st.top();
        st.pop();
        cout << node->data << " ";
        if (node->right) st.push(node->right);
        if (node->left) st.push(node->left);
    }
}

void inorderIterative(TreeNode* root) {
    stack<TreeNode*> st;
    TreeNode* curr = root;
    while (curr != nullptr || !st.empty()) {
        while (curr != nullptr) {
            st.push(curr);
            curr = curr->left;
        }
        curr = st.top();
        st.pop();
        cout << curr->data << " ";
        curr = curr->right;
    }
}

void postorderIterative(TreeNode* root) {
    if (root == nullptr) return;
    stack<TreeNode*> st1, st2;
    st1.push(root);
    while (!st1.empty()) {
        TreeNode* node = st1.top();
        st1.pop();
        st2.push(node);
        if (node->left) st1.push(node->left);
        if (node->right) st1.push(node->right);
    }
    while (!st2.empty()) {
        cout << st2.top()->data << " ";
        st2.pop();
    }
}

// ---------- 层序遍历(队列)----------
void levelorder(TreeNode* root) {
    if (root == nullptr) return;
    queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        TreeNode* curr = q.front();
        q.pop();
        cout << curr->data << " ";
        if (curr->left) q.push(curr->left);
        if (curr->right) q.push(curr->right);
    }
}

int main() {
    // 构建标准二叉树
    TreeNode* root = new TreeNode('A');
    root->left = new TreeNode('B');
    root->right = new TreeNode('C');
    root->left->left = new TreeNode('D');
    root->left->right = new TreeNode('E');
    root->right->left = new TreeNode('F');
    root->right->right = new TreeNode('G');

    cout << "=== 递归版本 ===" << endl;
    cout << "前序: "; preorderRecursive(root); cout << endl;
    cout << "中序: "; inorderRecursive(root); cout << endl;
    cout << "后序: "; postorderRecursive(root); cout << endl;

    cout << "=== 迭代版本 ===" << endl;
    cout << "前序: "; preorderIterative(root); cout << endl;
    cout << "中序: "; inorderIterative(root); cout << endl;
    cout << "后序: "; postorderIterative(root); cout << endl;

    cout << "=== 层序 ===" << endl;
    cout << "层序: "; levelorder(root); cout << endl;

    // 释放内存(这里省略,实际可以使用递归 delete)
    return 0;
}

输出如下(确保两种版本结果一致):

=== 递归版本 ===
前序: A B D E C F G 
中序: D B E A F C G 
后序: D E B F G C A 
=== 迭代版本 ===
前序: A B D E C F G 
中序: D B E A F C G 
后序: D E B F G C A 
=== 层序 ===
层序: A B C D E F G 

六、总结与相关指引

  1. 遍历的意义:把二叉树的节点排成一条线,是许多高级操作的基础(如查找、打印、序列化、表达式求值)。
  2. 记忆方法:前中后序看根的位置;层序看队列。
  3. 时间与空间:所有遍历都是 O(n) 时间。递归空间 O(h)(h 为树高),层序空间 O(w)(w 为最大宽度)。
  4. 常见应用
    • 中序遍历二叉搜索树能得到升序序列(比如在搜索引擎中排序)。
    • 前序遍历常用于复制树表达式树的前缀表示(比如数学公式转成计算机指令)。
    • 后序遍历用于删除树(先删除孩子再删除根)。
    • 层序遍历用于求树的宽度打印树形结构最短路径等。

接下来你可以学习

  • 如何用遍历结果还原一棵树(已知前序+中序 或 后序+中序)
  • 二叉树的深度与宽度计算
  • 二叉搜索树(BST)中查找、插入、删除
  • 堆(Heap)与优先队列(前序遍历就不适合堆了)
  • 树的其他遍历:N 叉树的遍历、图的 DFS 和 BFS

试着画一棵简单的树(比如只有三个节点),手动模拟四种遍历,再用代码跑一遍,你会发现原来树遍历就像排队报数一样简单!

例题精讲

1单选题

已知一棵二叉树的前序遍历序列为ABDCE,中序遍历序列为BDAEC,则该树的后序遍历序列是?

ADBECA
BDECAB
CBDACE
DECDBA
2判断题

给定一棵二叉树的后序遍历序列和中序遍历序列,可以唯一确定该二叉树的结构。

3填空题
补全下面递归中序遍历二叉树的代码。
void inorder(struct TreeNode* root) {
    if (root == NULL) return;
    inorder(root->left);
    ___
    inorder(root->right);
}
4单选题

以下关于二叉树遍历的说法中,正确的是?

A前序遍历中第一个被访问的节点一定是根节点。
B中序遍历中最后一个被访问的节点一定是根节点。
C后序遍历中第一个被访问的节点一定是根节点。
D层序遍历中最后一个被访问的节点一定是叶子节点。
5判断题

二叉树的层次遍历(层序遍历)序列唯一对应一棵二叉树。