一篇吃透树的遍历:递归与迭代的完整拆解

一篇吃透树的遍历:递归与迭代的完整拆解

摘要:树的遍历是数据结构的必修课。本文从二叉树的基本概念出发,用递归和迭代两种方式实现前序、中序、后序、层序遍历,每一步都配有详细的执行过程拆解,帮你彻底理解四种遍历的规律与差异。

📑 目录

  • 树的基本要点
  • 四种遍历方式概述
  • 递归法:最自然的实现
  • 迭代法:用栈和队列模拟递归
  • 递归与迭代的对比总结
  • 互动讨论

树的基本要点

二叉树是每个节点最多有两个子树的树结构。在开始遍历之前,先明确几个基本概念:

  • 根节点:树的顶层节点,没有父节点。
  • 叶子节点:没有子节点的节点。
  • 左子树 / 右子树:根节点下方的左右分支,本身也是一棵二叉树。
  • 深度:从根节点到某个节点的路径长度。

二叉树的定义本身就是递归的:一棵二叉树由根节点、左子树、右子树三部分组成,而左右子树也是二叉树。这个递归结构决定了递归是处理树最自然的思维方式。

为了便于理解后续的遍历过程,先定义一棵示例树:

text

markdown 复制代码
        1
       / \
      2   3
     / \   \
    4   5   6
  • 根节点是 1
  • 2 和 3 是 1 的左右孩子
  • 4 和 5 是 2 的左右孩子
  • 6 是 3 的右孩子

四种遍历方式概述

遍历方式 访问顺序 核心规律
前序遍历 根 → 左 → 右 根节点最先访问
中序遍历 左 → 根 → 右 根节点在中间访问
后序遍历 左 → 右 → 根 根节点最后访问
层序遍历 从上到下,从左到右 按层逐级访问

对于上面的示例树,四种遍历的结果:

  • 前序:1 → 2 → 4 → 5 → 3 → 6
  • 中序:4 → 2 → 5 → 1 → 3 → 6
  • 后序:4 → 5 → 2 → 6 → 3 → 1
  • 层序:1 → 2 → 3 → 4 → 5 → 6

递归法:最自然的实现

由于树的定义本身就是递归的,递归遍历代码极其简洁------直接按照访问顺序调用自身即可。

前序遍历(递归)

核心逻辑:对于当前节点,先访问它自己(记录值),再递归访问左子树,最后递归访问右子树。

c

scss 复制代码
void preorderRecur(struct TreeNode* root, int* result, int* returnSize) {
    if (!root) return;
    result[(*returnSize)++] = root->val;  // 1. 先访问根
    preorderRecur(root->left, result, returnSize);   // 2. 再遍历左子树
    preorderRecur(root->right, result, returnSize);  // 3. 最后遍历右子树
}

执行过程(示例树)

text

scss 复制代码
preorderRecur(1)
  → 记录 1
  → preorderRecur(2)
      → 记录 2
      → preorderRecur(4)
          → 记录 4
          → preorderRecur(NULL) 返回
          → preorderRecur(NULL) 返回
      → preorderRecur(5)
          → 记录 5
          → preorderRecur(NULL) 返回
          → preorderRecur(NULL) 返回
  → preorderRecur(3)
      → 记录 3
      → preorderRecur(NULL) 返回
      → preorderRecur(6)
          → 记录 6
          → preorderRecur(NULL) 返回
          → preorderRecur(NULL) 返回

结果:[1, 2, 4, 5, 3, 6]

中序遍历(递归)

核心逻辑:对于当前节点,先递归访问左子树,再访问自己,最后递归访问右子树。

c

scss 复制代码
void inorderRecur(struct TreeNode* root, int* result, int* returnSize) {
    if (!root) return;
    inorderRecur(root->left, result, returnSize);    // 1. 先遍历左子树
    result[(*returnSize)++] = root->val;             // 2. 再访问根
    inorderRecur(root->right, result, returnSize);   // 3. 最后遍历右子树
}

执行过程(示例树)

text

scss 复制代码
inorderRecur(1)
  → inorderRecur(2)
      → inorderRecur(4)
          → inorderRecur(NULL) 返回
          → 记录 4
          → inorderRecur(NULL) 返回
      → 记录 2
      → inorderRecur(5)
          → inorderRecur(NULL) 返回
          → 记录 5
          → inorderRecur(NULL) 返回
  → 记录 1
  → inorderRecur(3)
      → inorderRecur(NULL) 返回
      → 记录 3
      → inorderRecur(6)
          → inorderRecur(NULL) 返回
          → 记录 6
          → inorderRecur(NULL) 返回

结果:[4, 2, 5, 1, 3, 6]

后序遍历(递归)

核心逻辑:对于当前节点,先递归访问左子树,再递归访问右子树,最后访问自己。

c

scss 复制代码
void postorderRecur(struct TreeNode* root, int* result, int* returnSize) {
    if (!root) return;
    postorderRecur(root->left, result, returnSize);   // 1. 先遍历左子树
    postorderRecur(root->right, result, returnSize);  // 2. 再遍历右子树
    result[(*returnSize)++] = root->val;              // 3. 最后访问根
}

执行过程(示例树)

text

scss 复制代码
postorderRecur(1)
  → postorderRecur(2)
      → postorderRecur(4)
          → postorderRecur(NULL) 返回
          → postorderRecur(NULL) 返回
          → 记录 4
      → postorderRecur(5)
          → postorderRecur(NULL) 返回
          → postorderRecur(NULL) 返回
          → 记录 5
      → 记录 2
  → postorderRecur(3)
      → postorderRecur(NULL) 返回
      → postorderRecur(6)
          → postorderRecur(NULL) 返回
          → postorderRecur(NULL) 返回
          → 记录 6
      → 记录 3
  → 记录 1

结果:[4, 5, 2, 6, 3, 1]

层序遍历(递归)

层序遍历的递归实现与前三者不同------它不是按"访问顺序"递归,而是按"层级"递归:先处理第 0 层,再处理第 1 层,依此类推。递归函数需要额外接收一个参数 level,表示当前节点所在的层数。

核心逻辑:递归遍历整棵树,将每个节点放入对应层级的数组中。

c

scss 复制代码
void levelOrderRecurHelper(struct TreeNode* root, int level, int*** result, int* returnSize, int** returnColumnSizes) {
    if (!root) return;
    if (level >= *returnSize) {
        *result = (int**)realloc(*result, (level + 1) * sizeof(int*));
        (*returnColumnSizes) = (int*)realloc(*returnColumnSizes, (level + 1) * sizeof(int));
        (*returnColumnSizes)[level] = 0;
        (*result)[level] = NULL;
        (*returnSize)++;
    }
    (*returnColumnSizes)[level]++;
    (*result)[level] = (int*)realloc((*result)[level], (*returnColumnSizes)[level] * sizeof(int));
    (*result)[level][(*returnColumnSizes)[level] - 1] = root->val;
    levelOrderRecurHelper(root->left, level + 1, result, returnSize, returnColumnSizes);
    levelOrderRecurHelper(root->right, level + 1, result, returnSize, returnColumnSizes);
}

执行过程(示例树)

text

ini 复制代码
levelOrderRecurHelper(1, level=0)
  → 存入 result[0][0] = 1
  → 递归左子树(2, level=1)
      → 存入 result[1][0] = 2
      → 递归左子树(4, level=2)
          → 存入 result[2][0] = 4
      → 递归右子树(5, level=2)
          → 存入 result[2][1] = 5
  → 递归右子树(3, level=1)
      → 存入 result[1][1] = 3
      → 递归右子树(6, level=2)
          → 存入 result[2][2] = 6

结果:[[1], [2, 3], [4, 5, 6]]

注意:递归层序遍历需要在递归过程中动态扩展二维数组,在实现上比前三种复杂一些。在实际面试中,如果要求层序遍历,面试官通常期望看到迭代法(队列实现),因为它更直观。因此层序遍历我会在迭代法中重点展开。

递归法总结

遍历 核心动作顺序 代码行数
前序 访问 → 左 → 右 最少
中序 左 → 访问 → 右 最少
后序 左 → 右 → 访问 最少
层序 按层存储 较多(需动态扩维)

迭代法:用栈和队列模拟递归

递归的本质是函数调用栈 ------每一次递归调用都会在栈上压入一个帧。用显式栈可以模拟递归过程,同时避免递归过深导致的栈溢出风险。

四种遍历在迭代实现中分别使用两种数据结构:前、中、后序用 (LIFO),层序用队列(FIFO)。

前序遍历(迭代)

核心逻辑:利用栈的"后进先出"特性。先把根节点入栈,然后循环弹出栈顶节点并访问,访问后将右孩子和左孩子依次入栈。因为栈是后进先出,所以先入右孩子、再入左孩子,弹出时左孩子会先被处理,正好符合"先左后右"的顺序。

c

arduino 复制代码
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    struct TreeNode** stack = (struct TreeNode**)malloc(100 * sizeof(struct TreeNode*));
    int* result = (int*)malloc(100 * sizeof(int));
    int re = 0, size = 0;
    if (root) stack[size++] = root;
    while (size > 0) {
        struct TreeNode* q = stack[--size];      // 弹出栈顶节点
        result[re++] = q->val;                   // 访问
        if (q->right) stack[size++] = q->right;  // 先压右孩子
        if (q->left) stack[size++] = q->left;    // 再压左孩子
    }
    *returnSize = re;
    return result;
}

执行过程(示例树)

步骤 栈内容(从顶到底) 弹出 结果
1 [1] 1 [1]
2 [3, 2] 2 [1, 2]
3 [3, 5, 4] 4 [1, 2, 4]
4 [3, 5] 5 [1, 2, 4, 5]
5 [3] 3 [1, 2, 4, 5, 3]
6 [6] 6 [1, 2, 4, 5, 3, 6]

结果:[1, 2, 4, 5, 3, 6]

为什么先压右再压左:栈是后进先出的结构,如果先压左再压右,右孩子会先被弹出,导致访问顺序变成"根 → 右 → 左",不符合前序遍历的"根 → 左 → 右"。所以必须反过来------先压右、再压左。

中序遍历(迭代)

核心逻辑:中序遍历的迭代需要"一路向左走到底",把路径上的所有节点入栈,然后依次弹出访问,再转向右子树。

c

ini 复制代码
int* inorderTraversal(struct TreeNode* root, int* returnSize) {
    struct TreeNode** stack = (struct TreeNode**)malloc(100 * sizeof(struct TreeNode*));
    int* result = (int*)malloc(100 * sizeof(int));
    int top = 0, size = 0;
    struct TreeNode* q = root;
    while (q || top > 0) {
        while (q) {                          // 一路向左走到底
            stack[top++] = q;
            q = q->left;
        }
        q = stack[--top];                    // 弹出栈顶(最左节点)
        result[size++] = q->val;             // 访问
        q = q->right;                        // 转向右子树
    }
    *returnSize = size;
    return result;
}

执行过程(示例树)

text

ini 复制代码
初始:q = 1, stack = []
第1轮外循环:
  内循环:1入栈 → q=2 → 2入栈 → q=4 → 4入栈 → q=NULL
  弹出4 → result=[4] → q=5(4的右孩子,实际上为NULL)
第2轮外循环:
  内循环:q=NULL,跳过
  弹出2 → result=[4,2] → q=5
第3轮外循环:
  内循环:5入栈 → q=NULL
  弹出5 → result=[4,2,5] → q=NULL(5的右孩子)
第4轮外循环:
  弹出1 → result=[4,2,5,1] → q=3
第5轮外循环:
  内循环:3入栈 → q=6 → 6入栈 → q=NULL
  弹出6 → result=[4,2,5,1,6] → q=NULL
第6轮外循环:
  弹出3 → result=[4,2,5,1,6,3] → q=NULL

结果:[4, 2, 5, 1, 3, 6]

关键理解:中序遍历访问节点的时机是"左子树全部处理完、准备处理当前节点"的时候。所以先一路向左把路径存起来,当走到 NULL 时,说明当前节点的左子树已经处理完毕,可以弹出并访问当前节点,然后转向右子树。

后序遍历(迭代)

核心逻辑:使用两个栈。第一个栈用于"反向"遍历------先根、再右、再左,得到的顺序正好是前序遍历的镜像。第二个栈用于反转这个顺序,最终得到正确的后序遍历(左 → 右 → 根)。

c

ini 复制代码
int* postorderTraversal(struct TreeNode* root, int* returnSize) {
    struct TreeNode** stack1 = (struct TreeNode**)malloc(100 * sizeof(struct TreeNode*));
    struct TreeNode** stack2 = (struct TreeNode**)malloc(100 * sizeof(struct TreeNode*));
    int* result = (int*)malloc(100 * sizeof(int));
    int top1 = 0, top2 = 0, size = 0;
    if (root) stack1[top1++] = root;
    while (top1 > 0) {
        struct TreeNode* q = stack1[--top1];   // 从 stack1 弹出
        stack2[top2++] = q;                    // 压入 stack2
        if (q->left) stack1[top1++] = q->left;   // 先压左(和前序相反)
        if (q->right) stack1[top1++] = q->right; // 再压右
    }
    while (top2 > 0)
        result[size++] = stack2[--top2]->val;  // 从 stack2 弹出即为后序
    *returnSize = size;
    return result;
}

执行过程(示例树)

第一步:stack1 遍历,stack2 收集

步骤 stack1 弹出 stack2 入栈 stack1 入栈(先左后右)
1 1 stack2=1 入左2、右3 → stack1=3,2
2 2 stack2=1,2 入左4、右5 → stack1=3,5,4
3 4 stack2=1,2,4 无 → stack1=3,5
4 5 stack2=1,2,4,5 无 → stack1=3
5 3 stack2=1,2,4,5,3 入右6 → stack1=6
6 6 stack2=1,2,4,5,3,6 无 → stack1=\[\]

第二步:从 stack2 依次弹出

text

ini 复制代码
stack2: [1, 2, 4, 5, 3, 6](从栈顶到栈底)
弹出顺序:6 → 3 → 5 → 4 → 2 → 1
结果:[4, 5, 2, 6, 3, 1]

结果:[4, 5, 2, 6, 3, 1]

为什么这样能工作:stack1 的遍历顺序是"根 → 左 → 右"(先压左再压右,所以右先出)。stack2 的入栈顺序是 1、2、4、5、3、6,从栈顶弹出时正好反转,得到"左 → 右 → 根"的后序顺序。

层序遍历(迭代)

核心逻辑:使用队列(FIFO),而不是栈。每次从队头取出一个节点访问,将其左右孩子依次放入队尾。队列的"先进先出"特性天然保证按层逐级处理。

c

ini 复制代码
int** levelOrder(struct TreeNode* root, int* returnSize, int** returnColumnSizes) {
    struct TreeNode** queue = (struct TreeNode**)malloc(2000 * sizeof(struct TreeNode*));
    int** result = (int**)malloc(2000 * sizeof(int*));
    *returnColumnSizes = (int*)malloc(2000 * sizeof(int));
    int f = 0, t = 0, level = 0, times = 0;
    if (root) {
        queue[t++] = root;
        times++;
    }
    while (f < t) {
        int num = times;           // 当前层的节点数
        times = 0;                 // 重置,用于计数下一层
        (*returnColumnSizes)[level] = num;
        result[level] = (int*)malloc(num * sizeof(int));
        struct TreeNode* q;
        for (int i = 0; i < num; i++) {
            q = queue[f++];        // 出队
            result[level][i] = q->val;
            if (q->left) {
                queue[t++] = q->left;
                times++;
            }
            if (q->right) {
                queue[t++] = q->right;
                times++;
            }
        }
        level++;
    }
    *returnSize = level;
    return result;
}

times 变量的关键作用

times 记录的是当前层 的节点数量。在处理完当前层后,times 会被更新为下一层的节点数,用于控制下一轮的循环次数。这保证每一层的数据被正确地划分到各自的数组中。

执行过程(示例树)

text

ini 复制代码
初始:队列 [1], times=1, level=0

第1轮(level=0):num=1, times=0
  出队 1 → result[0][0]=1
  入队 2、3 → queue=[2,3], times=2
第2轮(level=1):num=2, times=0
  出队 2 → result[1][0]=2
  入队 4、5 → queue=[3,4,5], times=2
  出队 3 → result[1][1]=3
  入队 6 → queue=[4,5,6], times=3
第3轮(level=2):num=3, times=0
  出队 4 → result[2][0]=4
  出队 5 → result[2][1]=5
  出队 6 → result[2][2]=6

结果:[[1], [2, 3], [4, 5, 6]]

迭代法总结

遍历 数据结构 核心操作 关键点
前序 弹出即访问,先压右再压左 入栈顺序决定访问顺序
中序 一路向左入栈,弹出访问后转右 左子树入栈时机
后序 双栈 第一个栈遍历,第二个栈反转 两个栈接力完成反转
层序 队列 出队访问,入队孩子 times 控制分层

递归与迭代的对比总结

维度 递归法 迭代法
代码量 少,简洁优雅 多,需要手动维护栈/队列
可读性 高,直接对应数学定义 中,需要理解栈/队列的运作
空间复杂度 O(h),h 为树高(调用栈) O(h) 或 O(n)(显式栈/队列)
风险 树过高时可能栈溢出 无栈溢出风险
适用场景 理解遍历逻辑、树不太深 生产环境、需要严格控制的场景

四种遍历的规律速记

遍历 口诀 迭代核心
前序 根左右 先压右再压左
中序 左根右 一路向左入栈
后序 左右根 双栈反转
层序 从上到下,从左到右 队列 + times 分层

互动讨论

  1. 前序遍历中,为什么先压右孩子再压左孩子? 如果反过来会怎样?
  2. 中序遍历的迭代实现中,while (q || top > 0) 这个条件为什么是 || 而不是 &&
  3. 后序遍历能否只用单栈实现? 如果可以,思路是什么?
  4. 层序遍历中 times 变量的作用是什么? 如果不用 times,还有什么方法可以分层?
  5. 递归遍历和迭代遍历,你更倾向在面试中使用哪一种? 为什么?

📌 一点心得:递归是理解树遍历最自然的入口,迭代是生产环境更可靠的选择。把两者都吃透了,才算真正掌握了树的遍历。

相关推荐
Robot_Nav2 小时前
贪心算法、动态规划与 MPPI 算法结构相关力扣题目汇总
算法·贪心算法·动态规划
战族狼魂2 小时前
每日一课:算法系统学习路线
人工智能·算法·大模型·大语言模型
变量未定义~2 小时前
连通块中点的数量、堆箱子(4星)
算法
j7~3 小时前
【算法】专题二:滑动窗口之水果成蓝,找到字符串中所有字⺟异位词等算法题
c++·算法·滑动窗口·水果成蓝·最小字串覆盖·优选算法精选
木木子223 小时前
# 星座配对深度解析:Select 长列表选择器、矩阵评分算法与数据驱动建议
java·算法·华为·矩阵·harmonyos
大鱼>3 小时前
AI+货物追踪:跨境电商包裹追踪系统
人工智能·深度学习·算法·机器学习
大鱼>3 小时前
AI+货物追踪:全链路货物可视化追踪系统
人工智能·深度学习·算法·机器学习
大江东去浪淘尽千古风流人物3 小时前
【SLAM】slam高动态位姿估计全链路拆解
算法·面试·职场和发展·视觉里程计·slam·vio·大疆
想你依然心痛3 小时前
量子计算对嵌入式密码学的挑战与后量子算法准备
算法·密码学·量子计算