← 返回博客列表

一、树基础概念:二叉/满/完全/平衡/BST

树(Tree)是一种由 n 个有限节点组成的具有层次关系的集合。之所以叫"树",是因为它看起来像一棵倒挂的树——根朝上,叶朝下。树是图的特例:无环的连通图。

核心术语
根节点(Root):没有父节点的节点,一棵树只有一个根。
叶子节点(Leaf):没有子节点的节点。
节点的度(Degree):一个节点含有的子树个数。
树的高度/深度(Height/Depth):从根到最远叶子的路径上的节点数。

在算法题中,二叉树(Binary Tree)是最常见的结构——每个节点最多有两个子节点,分别称为左子节点和右子节点。下面我们通过图解对比四种重要的二叉树变体。

四种常见二叉树结构对比 ① 普通二叉树 1 2 3 4 每个节点最多两个子节点 无其他约束 ② 满二叉树 1 2 3 4 5 6 7 每层都满:节点数 = 2^h - 1 叶子只在最后一层 ③ 完全二叉树 1 2 3 4 5 6 除最后一层外每层都满 最后一层节点靠左连续 ④ 二叉搜索树 BST 8 3 10 1 6 14 左子树所有节点 < 根 < 右子树 中序遍历结果严格递增 结构对比总结 满二叉树 ⊂ 完全二叉树 ⊂ 普通二叉树;BST 是附加了"节点大小关系"的普通二叉树。 完全二叉树可用数组紧凑存储(下标 i 的左孩子 2i+1、右孩子 2i+2),是堆结构的基础。 平衡二叉树(AVL/红黑树)是 BST 的优化:左右子树高度差 ≤ 1,保证 O(log n) 操作。 BST 退化成链表时复杂度降为 O(n),因此实际工程中常用平衡 BST。
图 1:四种二叉树结构对比(普通/满/完全/BST)

1.1 TreeNode 的 C++ 定义

LeetCode 中二叉树节点使用标准定义,我们在整篇文章中统一使用:

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode* l, TreeNode* r) : val(x), left(l), right(r) {}
};

二、前中后序 DFS 遍历原理

深度优先搜索(DFS)沿着一条分支尽可能深地走,走到底再回溯。对二叉树而言,DFS 有三种变体,区别仅在于"访问当前节点"在什么时机执行:

DFS 三序核心定义
前序(Pre-order):根 → 左 → 右(先访问根,再递归左右)
中序(In-order):左 → 根 → 右(左递归完访问根,再递归右)
后序(Post-order):左 → 右 → 根(左右都递归完才访问根)

"前/中/后"指的就是根节点被访问的位置:在最前面就是前序,在中间就是中序,在最后面就是后序。下面用同一棵树直观对比三种遍历的访问顺序。

DFS 三序遍历对同一棵树的访问顺序对比 A B C D E F G 前序 Pre-order 根→左→右: A B D E C F G 红色=当前根节点第一个被访问 中序 In-order 左→根→右: D B E A F C G 对 BST 而言中序就是升序序列 后序 Post-order 左→右→根: D E B F G C A 删树/统计高度等需要"先算孩子再算根"的场景
图 2:DFS 三序遍历对同一棵树的访问节点顺序对比

2.1 递归栈展开过程

递归的本质是系统帮你维护了一个调用栈。理解递归遍历的关键,是理解每次函数调用时"入栈、递归、回溯"的全过程。下面以中序遍历为例,可视化递归栈的变化:

中序遍历递归栈展开过程(以节点 A 为根) 树结构 A B C 步骤 1:inorder(A) 调用栈: inorder(A) ① 访问左 → 调用 inorder(B) 步骤 2:进入 B 调用栈: inorder(A) inorder(B) B.left 为空 → 访问 B.val="B" → 调用 inorder(B.right) 步骤 3:B 回溯 调用栈: inorder(A) B.right 空 → B 函数返回 输出序列:[B] 回到 A:访问 A.val="A" 步骤 4:进入 C 调用栈: inorder(A) inorder(C) C.left 空 → 访问 C.val="C" C.right 空 → 返回 输出序列:[B, A, C] 步骤 5:全部完成 调用栈:(空) 中序结果:B → A → C 时间复杂度 O(n):每节点进栈出栈各一次 空间复杂度 O(h):栈深 = 树高,最坏 O(n)
图 3:中序遍历递归栈的展开与回溯过程

三、示例一:LeetCode 144/94/145 三序遍历

题目:分别实现二叉树的前序、中序、后序遍历,返回节点值的数组。要求同时给出递归和迭代(手动栈)两种实现。

💡 为什么要掌握迭代写法?
递归虽然简洁,但在树极深时会爆栈(Stack Overflow)。迭代写法用手动栈,更稳定、更能体现你对遍历过程的理解,面试常考。

3.1 完整 C++ 代码(递归 + 迭代)

#include <vector>
#include <stack>
#include <algorithm>
using namespace std;

// ===== 公共定义 =====
struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// ============================================================
// 1. 前序遍历(LeetCode 144):根 → 左 → 右
// ============================================================

// 递归写法
void preRecur(TreeNode* root, vector<int>& res) {
    if (root == nullptr) return;           // 空节点直接返回
    res.push_back(root->val);               // ① 先访问根
    preRecur(root->left, res);              // ② 再递归左子树
    preRecur(root->right, res);             // ③ 最后递归右子树
}
vector<int> preorderTraversal(TreeNode* root) {
    vector<int> res;
    preRecur(root, res);
    return res;
}

// 迭代写法(栈)
vector<int> preorderIter(TreeNode* root) {
    vector<int> res;
    if (root == nullptr) return res;
    stack<TreeNode*> st;
    st.push(root);
    while (!st.empty()) {
        TreeNode* node = st.top(); st.pop();
        res.push_back(node->val);           // 出栈即访问
        // 注意:栈 LIFO,为了"左先于右"出栈,必须先压右再压左
        if (node->right) st.push(node->right);
        if (node->left)  st.push(node->left);
    }
    return res;
}

// ============================================================
// 2. 中序遍历(LeetCode 94):左 → 根 → 右
// ============================================================

// 递归写法
void inRecur(TreeNode* root, vector<int>& res) {
    if (root == nullptr) return;
    inRecur(root->left, res);               // ① 先递归左
    res.push_back(root->val);               // ② 左空了才访问根
    inRecur(root->right, res);              // ③ 最后递归右
}
vector<int> inorderTraversal(TreeNode* root) {
    vector<int> res;
    inRecur(root, res);
    return res;
}

// 迭代写法(指针 + 栈,经典"一路向左"法)
vector<int> inorderIter(TreeNode* root) {
    vector<int> res;
    stack<TreeNode*> st;
    TreeNode* cur = root;
    while (cur || !st.empty()) {
        // 第一步:一路向左,沿路节点全部入栈
        while (cur) {
            st.push(cur);
            cur = cur->left;
        }
        // 第二步:左走到头了,栈顶就是"最左未访问"节点
        cur = st.top(); st.pop();
        res.push_back(cur->val);            // 访问根
        cur = cur->right;                   // 转向右子树,下一轮重复
    }
    return res;
}

// ============================================================
// 3. 后序遍历(LeetCode 145):左 → 右 → 根
// ============================================================

// 递归写法
void postRecur(TreeNode* root, vector<int>& res) {
    if (root == nullptr) return;
    postRecur(root->left, res);             // ① 左
    postRecur(root->right, res);            // ② 右
    res.push_back(root->val);               // ③ 根(最后)
}
vector<int> postorderTraversal(TreeNode* root) {
    vector<int> res;
    postRecur(root, res);
    return res;
}

// 迭代写法:技巧法——"根右左"入栈,然后 reverse 得到"左右根"
vector<int> postorderIter(TreeNode* root) {
    vector<int> res;
    if (root == nullptr) return res;
    stack<TreeNode*> st;
    st.push(root);
    while (!st.empty()) {
        TreeNode* node = st.top(); st.pop();
        res.push_back(node->val);           // 按"根→右→左"顺序收集
        if (node->left)  st.push(node->left);   // 先压左,后出
        if (node->right) st.push(node->right);  // 后压右,先出
    }
    reverse(res.begin(), res.end());       // 反转 = 左→右→根
    return res;
}
⚠ 迭代后序的常见坑
后序是三序中迭代最难写的。推荐掌握上面的"反转技巧法",简洁可靠。面试若要求"不 reverse",可用"visited 标记法"记录节点右子树是否已处理过。

四、层序 BFS 遍历原理

广度优先搜索(BFS)又叫"层序遍历",它从上到下、从左到右,一层一层地访问节点。BFS 的核心数据结构是队列(Queue)——FIFO 的特性正好保证"先入队的层先被处理"。

BFS 层序核心步骤
① 初始化队列,将根节点入队
② 每轮循环开始时记录当前队列大小 size(这就是本层节点数)
③ 连续弹出 size 次,把弹出节点的子节点依次入队
④ 重复 ②③ 直到队列为空
BFS 层序遍历:队列逐层出入队过程 1 2 3 4 5 6 7 共 3 层 第 1 轮(处理第 0 层) 队列初始: 1 size=1,弹出 1 收集本层:[1] 1 的孩子 2、3 入队 队列变为: 2 3 第 2 轮(处理第 1 层) size=2,连弹 2 个: 弹出 2 → 入队 4、5 弹出 3 → 入队 6、7 收集本层:[2, 3] 队列变为: 4 5 6 7 第 3 轮(处理第 2 层) size=4,连弹 4 个: 4、5、6、7 均是叶子 没有新节点入队 收集本层:[4, 5, 6, 7] 最终:[[1],[2,3],[4,5,6,7]]
图 4:BFS 层序遍历——队列逐层出入队过程

4.1 BFS 与 DFS 对比

维度 DFS(前/中/后序) BFS(层序)
数据结构 栈 Stack(系统栈或手动栈) 队列 Queue
访问顺序 一条路走到底再回溯 从上到下一层一层走
典型应用 路径问题、回溯、拓扑排序 求最短路径、层信息(深度/宽度)
时间复杂度 O(n)——每节点访问一次 O(n)——每节点入队出队各一次
空间复杂度 O(h),h = 树高(最坏 O(n)) O(w),w = 最宽层节点数(完全二叉树最坏 O(n/2))
退化场景 树退化成链时,栈深 = n,爆栈风险 完美二叉树时队列大小 = n/2

五、示例二:LeetCode 102 层序遍历

题目(LeetCode 102):给定二叉树根节点 root,返回其节点值的层序遍历结果(即按层分组,从上到下、每层从左到右)。

5.1 过程图

LeetCode 102 示例:root = [3,9,20,null,null,15,7] 3 9 20 15 7 → 返回二维数组: 第 0 层:[3] 第 1 层:[9, 20] 第 2 层:[15, 7]
图 5:LeetCode 102 层序遍历输入输出示意

5.2 C++ 完整代码

#include <vector>
#include <queue>
using namespace std;

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// LeetCode 102:二叉树的层序遍历
vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> res;     // 结果二维数组:每层一个 vector
    if (root == nullptr) return res;

    queue<TreeNode*> q;             // BFS 的核心:队列
    q.push(root);

    while (!q.empty()) {
        int size = q.size();         // 关键:固定本层节点数!
        vector<int> level;          // 本层的收集数组

        // 只处理本层的 size 个节点,多一个都不弹
        for (int i = 0; i < size; ++i) {
            TreeNode* node = q.front();
            q.pop();
            level.push_back(node->val);

            // 孩子入队,作为下一层的预备
            if (node->left)  q.push(node->left);
            if (node->right) q.push(node->right);
        }
        res.push_back(level);         // 本层完成,加入结果
    }
    return res;
}
💡 BFS 模板精髓
每次循环开头先读取 q.size() 作为本层数量,然后只弹 size 个——这是"按层分组"的关键。如果不记录 size,新入队的孩子就会和本层节点混在一起,分不清层级。

六、BST 二叉搜索树原理

二叉搜索树(Binary Search Tree, BST)是一种"自带有序性"的二叉树。它的核心性质只有一条,但衍生出强大的查找、插入、删除能力:

BST 核心性质
对任意节点,左子树所有节点的 val < 该节点 val < 右子树所有节点的 val。

由此直接推出:BST 的中序遍历结果是严格递增序列。这是解 BST 题最常用的结论!

6.1 BST 插入过程

插入思路:从根开始比较,小于当前节点就往左走,大于就往右走,走到空位置就把新节点挂上去。

BST 插入过程:在树中插入新节点 5 原树: 8 3 10 1 6 4 插入 5 → 路径:8 → 3 → 6 → 4 的右孩子位置 ① 5 < 8,走左到 3 ② 5 > 3,走右到 6 ③ 5 < 6,走左到 4 ④ 5 > 4,4.right 空 → 挂这里! 5 新节点→ 插入后: 8 3 10 1 6 4 5
图 6:BST 插入新节点 5 的路径与结果

6.2 BST 查找与验证

BST 查找:搜索值 12 非 BST(反例):违反大小关系 8 3 10 14 13 12>8 走右 12>10 走右 12<14 走左 13 ≠ 12,且 13 是叶子 → 返回 false 时间复杂度 O(h) = O(log n)~O(n) 8 15 5 6 4 ❌ 15>8 在左子树 ❌ 5<8 在右子树 ❌ 6<15 却在左 中序结果:6,15,4,8,5 → 非递增 结论:不是 BST!
图 7:BST 查找过程(左)与非 BST 反例分析(右)

七、示例三:LeetCode 98 验证 BST

题目(LeetCode 98):给定一个二叉树,判断其是否是一个有效的二叉搜索树。

7.1 思路一:中序有序性证明

最优雅的解法。由 BST 定义可直接推出以下等价关系:

核心定理
一棵二叉树是 BST ⟺ 其中序遍历序列严格递增(无相等值)。

证明 ⇒(必要性):数学归纳法。假设对任意子树成立,中序=左中序+根+右中序,由 BST 定义 左中序所有值 < 根 < 右中序所有值,且各自内部递增,故整体严格递增。
证明 ⇐(充分性):若中序严格递增,则对任意节点,其左子树所有节点值在中序中位于它之前(更小),右子树所有节点值在中序中位于它之后(更大),故满足 BST 定义。

因此只需中序遍历并记录前一个值,若出现 cur.val ≤ prev 就返回 false。

7.2 完整 C++ 代码

#include <stack>
#include <climits>
using namespace std;

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// 方法 1:中序遍历 + 记录前驱(迭代写法,避免递归爆栈)
bool isValidBST(TreeNode* root) {
    if (root == nullptr) return true;

    stack<TreeNode*> st;
    TreeNode* cur = root;
    long long prev = (long long)INT_MIN - 1;  // 用 long long 防 INT_MIN 越界

    while (cur || !st.empty()) {
        // 一路向左入栈(标准中序迭代模板)
        while (cur) {
            st.push(cur);
            cur = cur->left;
        }
        cur = st.top(); st.pop();

        // ★ 关键检查:当前值必须严格大于前一个
        if ((long long)cur->val <= prev) {
            return false;
        }
        prev = cur->val;  // 更新前驱

        cur = cur->right; // 转向右子树
    }
    return true;
}

// 方法 2:递归 + 上下界(另一种经典思路,供对比)
// 每个节点必须满足 (lowerBound, upperBound) 开区间
bool check(TreeNode* node, long long lower, long long upper) {
    if (node == nullptr) return true;
    // 当前节点的值必须严格落在 (lower, upper) 之内
    if (node->val <= lower || node->val >= upper) return false;
    // 递归左:上界变成当前值;递归右:下界变成当前值
    return check(node->left, lower, node->val)
        && check(node->right, node->val, upper);
}
bool isValidBST2(TreeNode* root) {
    return check(root, (long long)INT_MIN - 1, (long long)INT_MAX + 1);
}
⚠ 常见坑:INT_MIN 边界
如果节点 val 可能取到 INT_MIN,用 prev = INT_MIN 初始化会出错(第一个节点等于 prev 被误判)。解决方法:① 用 long long 开区间;② 或用指针 TreeNode* prev = nullptr 只在非空时比较。

八、示例四:LeetCode 701 BST 插入

题目(LeetCode 701):给定 BST 根节点和要插入的值 val,将值插入 BST 并返回新的根。保证新值和原 BST 任一节点值都不同。

8.1 插入过程可视化

BST 插入 25:递归过程中每一层的返回值传递 原 BST(根=40):40 的左=20,右=60;20 的左=10,右=30 第 1 层 insert(40, 25):25 < 40 → 递归左孩子 insert(20, 25) 40.left = 返回值 第 2 层 insert(20, 25):25 > 20 → 递归右孩子 insert(30, 25) 20.right = 返回值 第 3 层 insert(30, 25):25 < 30 → 递归左孩子 insert(null, 25) 30.left = 返回值 第 4 层 insert(null, 25):空!return new TreeNode(25) ← 创建新节点并向上传递 new(25) 30 ← 20 ← 40 ←
图 8:BST 插入的递归过程——返回值把新节点一层层"挂"到父节点上

8.2 C++ 完整代码

#include <queue>
using namespace std;

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// LeetCode 701:二叉搜索树中的插入操作(递归)
TreeNode* insertIntoBST(TreeNode* root, int val) {
    // ★ 递归终止:走到空位置,创建新节点并返回给父节点
    if (root == nullptr) {
        return new TreeNode(val);
    }

    // val 比当前小 → 应该插入到左子树,于是递归后让 root.left 接住结果
    if (val < root->val) {
        root->left = insertIntoBST(root->left, val);
    }
    // val 比当前大 → 应该插入到右子树,同理让 root.right 接住
    else if (val > root->val) {
        root->right = insertIntoBST(root->right, val);
    }

    // 注意:题目保证 val 不在树中,所以不会遇到 == 的情况
    return root;  // 当前根向上返回,给上一层"接"好
}

// 另一种写法:迭代(用 parent 指针跟踪父节点)
TreeNode* insertIntoBSTIter(TreeNode* root, int val) {
    if (root == nullptr) return new TreeNode(val);

    TreeNode* cur = root;
    TreeNode* parent = nullptr;  // 记录 cur 的父节点

    // 找到插入点:cur 变成 nullptr 时,parent 就是"要挂新节点的父亲"
    while (cur != nullptr) {
        parent = cur;
        if (val < cur->val) cur = cur->left;
        else                 cur = cur->right;
    }

    // 判断挂在左还是右
    if (val < parent->val) parent->left  = new TreeNode(val);
    else                   parent->right = new TreeNode(val);

    return root;
}
💡 "接住返回值"的递归模式
BST 的插入、删除都常用这个模式:root->left = dfs(root->left)。这种写法让你不用额外记录 parent 指针——返回值本身就承担了"把修改后的子树接回父节点"的职责。非常优雅,务必掌握。

九、总结

树是算法面试的高频专题,核心就两条主线:遍历(DFS/BFS)与BST 性质。下面用一张表把本文所有知识收束起来。

树算法核心对比总结表

算法/结构 核心思想 数据结构 时间复杂度 识别信号 / 典型题
前序 DFS
根→左→右
先访问根再深入 栈(递归系统栈 / 手动栈) O(n)
O(h) 空间
复制树、求路径、先序序列化
LeetCode 144
中序 DFS
左→根→右
左走完才访问根 同上 O(n)
O(h) 空间
对 BST 等于升序序列!
LeetCode 94、98 验证BST、BST 第 k 小
后序 DFS
左→右→根
孩子都处理完才处理根 同上(或反转技巧) O(n)
O(h) 空间
求高度、删树、自底向上汇总
LeetCode 145、104 最大深度、110 平衡树
层序 BFS 逐层处理,队列 FIFO Queue O(n)
O(w) 空间 w=最宽层
"按层分组""最短路径""深度"
LeetCode 102、103 锯齿、199 右视图、111 最小深度
BST 查找 val 小走左大走右 无须额外结构 平均 O(log n)
最坏 O(n)
在 BST 中搜索目标值
LeetCode 700
BST 插入 走到空位挂新节点 递归 / 迭代 parent 平均 O(log n)
最坏 O(n)
"接住返回值"递归模式
LeetCode 701
BST 验证 中序严格递增
或上下界区间检查
栈 / 递归 O(n) 判断一棵树是不是 BST
LeetCode 98

进一步刷题建议

🎯 记忆口诀
"前中后看根在哪,BFS 用队列按层刷;BST 题找中序,有序递增就对啦。"
递归是首选、迭代用栈模拟、层序固定 size;BST 善用中序有序性质,插入删除用"接住返回值"的递归模式。

树是最能体现"递归思想"的数据结构,把这篇文章的所有代码和 SVG 图在脑子里过一遍,再刷完上面的推荐题——你会发现,90% 的树题都逃不出这几个模板的范围。