二叉树的遍历:前序、中序、后序与层序遍历

二叉树遍历,说白了就是按照某种规则,把树里的每个节点都访问一遍。我刚开始学数据结构时,总觉得这玩意儿抽象得很,不就是走一遍吗?后来在项目中写一个表达式解析器,才发现遍历顺序选错了,整个逻辑都得重来。嗯,今天咱们就把这四种遍历方式彻底讲透。

一、递归遍历:前序、中序、后序

递归遍历的核心思想其实很简单:把一棵树拆成根、左子树、右子树三部分。访问顺序不同,就产生了三种遍历方式。我个人习惯把递归遍历想象成「打印根节点的时机」——你什么时候打印根节点,就是什么遍历。

遍历方式 访问顺序 打印根节点时机 典型应用
前序遍历 根 → 左 → 右 先打印根,再递归左右 复制树、序列化
中序遍历 左 → 根 → 右 递归左子树后打印根 二叉搜索树排序输出
后序遍历 左 → 右 → 根 递归完左右子树再打印根 删除树、计算目录大小

1. 前序遍历(Preorder)

前序遍历的顺序是:先访问根节点,再遍历左子树,最后遍历右子树。我在项目中遇到过一个问题:需要把一棵二叉树序列化成字符串存到文件里,用的就是前序遍历。为什么?因为根在前,反序列化时能先重建根节点,再递归重建左右子树,逻辑最清晰。

void preorder(TreeNode* root) {
    if (root == NULL) return;  // 空树直接返回
    
    printf("%d ", root->data);  // 先访问根
    preorder(root->left);       // 再遍历左子树
    preorder(root->right);      // 最后遍历右子树
}
小技巧:前序遍历的递归代码,你只要记住「打印根节点的代码在递归调用之前」就行了。我曾经面试过一个候选人,他死活记不住三种遍历的区别,我就告诉他:看printf的位置,在递归前就是前序,在递归中间就是中序,在递归后就是后序。

2. 中序遍历(Inorder)

中序遍历的顺序是:先遍历左子树,再访问根节点,最后遍历右子树。你想想看,如果这是一棵二叉搜索树(BST),中序遍历的结果是什么?没错,就是升序序列。我在做数据库索引模块时,需要把B+树叶子节点的数据按顺序输出,底层逻辑其实就是中序遍历。

void inorder(TreeNode* root) {
    if (root == NULL) return;
    
    inorder(root->left);        // 先遍历左子树
    printf("%d ", root->data);  // 再访问根
    inorder(root->right);       // 最后遍历右子树
}
避坑指南:我曾经在嵌入式设备上写中序遍历,递归深度太深导致栈溢出。树的高度如果超过几百层,递归就不太靠谱了。这时候要么改用迭代方式(自己维护栈),要么检查一下树是否平衡。

3. 后序遍历(Postorder)

后序遍历的顺序是:先遍历左子树,再遍历右子树,最后访问根节点。说白了就是「先处理孩子,再处理父亲」。这个顺序在什么场景下特别有用?删除整棵树的时候。你得先把左右子树都删干净了,才能删根节点,否则会内存泄漏。

void postorder(TreeNode* root) {
    if (root == NULL) return;
    
    postorder(root->left);      // 先遍历左子树
    postorder(root->right);     // 再遍历右子树
    printf("%d ", root->data);  // 最后访问根
}
注意:后序遍历的递归代码中,printf在最后一行。很多初学者会搞混,写成前序或中序。我的建议是:写完后自己画一棵小树,手动模拟一遍递归过程,比死记硬背强一百倍。

二、层序遍历:队列实现

层序遍历和前三种不一样,它不是深度优先,而是广度优先。说白了就是一层一层地往下走,先访问第一层所有节点,再访问第二层,以此类推。实现层序遍历,队列是标配。

为什么会用到队列?你想想看,访问完一个节点后,它的左右孩子需要「排队等待」被访问。这不就是先进先出(FIFO)吗?队列天然适合这个场景。

void levelorder(TreeNode* root) {
    if (root == NULL) return;
    
    Queue* q = createQueue();  // 创建队列
    enqueue(q, root);          // 根节点入队
    
    while (!isEmpty(q)) {
        TreeNode* cur = dequeue(q);  // 出队一个节点
        printf("%d ", cur->data);    // 访问它
        
        if (cur->left != NULL)
            enqueue(q, cur->left);   // 左孩子入队
        if (cur->right != NULL)
            enqueue(q, cur->right);  // 右孩子入队
    }
    
    destroyQueue(q);
}
核心逻辑:层序遍历的代码其实就三步——出队、访问、左右孩子入队。循环直到队列为空。我在做嵌入式GUI的控件树渲染时,就用层序遍历来计算每个控件的Y坐标偏移量,因为同一层的控件需要对齐。

三、四种遍历对比与知识体系

下面这张图,是我自己总结的二叉树遍历知识体系。你看一眼就能明白四种遍历之间的关系和区别。

二叉树遍历知识体系 二叉树遍历 深度优先(DFS) 广度优先(BFS) 前序(根左右) 中序(左根右) 后序(左右根) 层序(队列) 递归实现 递归实现 递归实现 队列实现 递归三兄弟:前序、中序、后序 | 层序靠队列

四、实战经验总结

四种遍历方式,在实际项目中各有各的用武之地。我简单列一下,方便你以后选型时参考:

  • 前序遍历:适合复制树结构、序列化/反序列化。因为根在前,重建时能先确定根。
  • 中序遍历:二叉搜索树的排序输出。我在做嵌入式设备的配置参数管理时,用中序遍历把树形配置项按顺序打印出来。
  • 后序遍历释放树的内存、计算目录大小。先处理孩子再处理父亲,逻辑上最安全。
  • 层序遍历求树的宽度、按层打印、计算最短路径。我在做迷宫寻路算法时,BFS(层序思想)就是找最短路径的利器。
我的建议:初学者不要急着背代码。先画一棵树,用手模拟一遍遍历过程,把访问顺序写下来。然后对照代码看,每一步对应哪一行。这样学一遍,比抄十遍代码都管用。

好了,二叉树遍历的四种方式就讲到这里。递归实现的前序、中序、后序,加上队列实现的层序,覆盖了绝大多数实际开发场景。下次你在项目中遇到树形结构,不妨想想:我到底需要哪种顺序?想清楚了再动手,代码写起来就顺了。


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