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

0 阅读12分钟

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

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

📑 目录

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

树的基本要点

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

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

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

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

text

        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

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

preorderRecur(1)
  → 记录 1preorderRecur(2)
      → 记录 2preorderRecur(4)
          → 记录 4preorderRecur(NULL) 返回
          → preorderRecur(NULL) 返回
      → preorderRecur(5)
          → 记录 5preorderRecur(NULL) 返回
          → preorderRecur(NULL) 返回
  → preorderRecur(3)
      → 记录 3preorderRecur(NULL) 返回
      → preorderRecur(6)
          → 记录 6preorderRecur(NULL) 返回
          → preorderRecur(NULL) 返回

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

中序遍历(递归)

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

c

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

inorderRecur(1)
  → inorderRecur(2)
      → inorderRecur(4)
          → inorderRecur(NULL) 返回
          → 记录 4inorderRecur(NULL) 返回
      → 记录 2inorderRecur(5)
          → inorderRecur(NULL) 返回
          → 记录 5inorderRecur(NULL) 返回
  → 记录 1inorderRecur(3)
      → inorderRecur(NULL) 返回
      → 记录 3inorderRecur(6)
          → inorderRecur(NULL) 返回
          → 记录 6inorderRecur(NULL) 返回

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

后序遍历(递归)

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

c

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

postorderRecur(1)
  → postorderRecur(2)
      → postorderRecur(4)
          → postorderRecur(NULL) 返回
          → postorderRecur(NULL) 返回
          → 记录 4postorderRecur(5)
          → postorderRecur(NULL) 返回
          → postorderRecur(NULL) 返回
          → 记录 5
      → 记录 2postorderRecur(3)
      → postorderRecur(NULL) 返回
      → postorderRecur(6)
          → postorderRecur(NULL) 返回
          → postorderRecur(NULL) 返回
          → 记录 6
      → 记录 3
  → 记录 1

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

层序遍历(递归)

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

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

c

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

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

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

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

初始:q = 1, stack = []
第1轮外循环:
  内循环:1入栈 → q=22入栈 → q=44入栈 → q=NULL
  弹出4 → result=[4] → q=54的右孩子,实际上为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=66入栈 → 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

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 入栈(先左后右)
11stack2=[1]入左2、右3 → stack1=[3,2]
22stack2=[1,2]入左4、右5 → stack1=[3,5,4]
34stack2=[1,2,4]无 → stack1=[3,5]
45stack2=[1,2,4,5]无 → stack1=[3]
53stack2=[1,2,4,5,3]入右6 → stack1=[6]
66stack2=[1,2,4,5,3,6]无 → stack1=[]

第二步:从 stack2 依次弹出

text

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

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

初始:队列 [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. 递归遍历和迭代遍历,你更倾向在面试中使用哪一种?  为什么?

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