算法

二叉树

递归分解是核心——解决左子树、解决右子树、合并结果,几乎覆盖所有树题

概览

二叉树问题几乎都遵循同一个模板:递归处理左子树,递归处理右子树,然后在当前节点合并结果。关键在于明确「函数返回值的语义」——它应该向父节点传递什么信息,然后父节点如何利用这个信息。

万能递归模板

在写任何树题前,先明确三件事:① 递归函数返回值代表什么;② base case(空节点)返回什么;③ 当前节点如何用左右子树的返回值计算自己的答案。

ReturnType solve(TreeNode* root) {
    if (!root) return BASE_CASE;          // ① base case
    auto left  = solve(root->left);       // recurse left
    auto right = solve(root->right);      // recurse right
    return MERGE(root->val, left, right); // ③ combine
}

三种遍历顺序

遍历顺序典型用途
前序(Pre-order)根 → 左 → 右序列化树结构、构建前缀表达式
中序(In-order)左 → 根 → 右BST 中序 = 升序序列,验证/枚举 BST
后序(Post-order)左 → 右 → 根树形 DP(子树返回值 → 父节点合并)

复杂度

时间 O(n)每个节点恰好访问一次
空间 O(h)递归栈深度 = 树高 h;最坏(链状树)O(n),平衡树 O(log n)

DFS 遍历(前中后序)

递归实现简洁自然;迭代实现用显式栈模拟调用栈,在树很深时可避免栈溢出。面试中迭代中序是高频考点。

递归实现(三种一体)

// Pre-order: Root → Left → Right
void preorder(TreeNode* root, vector<int>& res) {
    if (!root) return;
    res.push_back(root->val);    // visit root FIRST
    preorder(root->left, res);
    preorder(root->right, res);
}

// In-order: Left → Root → Right
void inorder(TreeNode* root, vector<int>& res) {
    if (!root) return;
    inorder(root->left, res);
    res.push_back(root->val);    // visit root IN MIDDLE
    inorder(root->right, res);
}

// Post-order: Left → Right → Root
void postorder(TreeNode* root, vector<int>& res) {
    if (!root) return;
    postorder(root->left, res);
    postorder(root->right, res);
    res.push_back(root->val);    // visit root LAST (subtrees already resolved)
}

迭代中序(最重要)

用显式栈模拟「一路向左,到底后弹出、访问、转右」的过程。cur 指针控制当前节点,不为 null 时持续压栈并向左走;为 null 时弹出栈顶访问并向右走。

迭代中序步骤

  1. 初始化 cur = root,stack<TreeNode*> st
  2. 外层循环:cur 不为 null 或栈不为空时继续
  3. 内层循环:cur 不为 null 时持续压栈,cur = cur->left
  4. 弹出栈顶:cur = st.top(); st.pop(); 访问 cur->val
  5. 向右走:cur = cur->right,回到外层循环

C++ 实现

// Iterative in-order — avoids stack overflow on deep trees
vector<int> inorderIterative(TreeNode* root) {
    vector<int> res;
    stack<TreeNode*> st;
    TreeNode* cur = root;
    while (cur || !st.empty()) {
        while (cur) { st.push(cur); cur = cur->left; } // go all the way left
        cur = st.top(); st.pop();
        res.push_back(cur->val);   // visit
        cur = cur->right;          // MUST move right (or infinite loop!)
    }
    return res;
}
int maxDepth(TreeNode* root) {
    if (!root) return 0;
    return 1 + max(maxDepth(root->left), maxDepth(root->right));
}

常见错误

迭代中序死循环访问节点后必须 cur = cur->right,否则会反复弹出同一个节点
后序迭代后序迭代较复杂,常用「前序的镜像(根右左)再逆序」技巧:preorder 存 right 再 left,最后 reverse
递归深度树退化为链表时递归深度 O(n) 可能栈溢出,面试可提出改用迭代
练习:二叉树的最大深度代码练习
测试用例(5 个)
用例 1
输入:3 9 20 null null 15 7
期望:3
用例 2
输入:1 null 2
期望:2
用例 3
输入:1
期望:1
用例 4
输入:null
期望:0
用例 5
输入:1 2 3 4 5
期望:3
Iterative Inorder Traversal代码练习
测试用例(5 个)
用例 1
输入:4 2 7 1 3 6 9
期望:1 2 3 4 6 7 9
用例 2
输入:1 null 2 null 3
期望:1 2 3
用例 3
输入:1
期望:1
用例 4
输入:3 1 2
期望:1 3 2
用例 5
输入:5 3 6 2 4 null null 1
期望:1 2 3 4 5 6

BFS 层序遍历

用队列按层处理节点:每轮循环前记录当前队列长度 sz,只处理这 sz 个节点(属于同一层),处理时将子节点入队。

核心模板

queue'<'TreeNode*'>' q; q.push(root)。外层 while (!q.empty()) 每次迭代处理一整层:int sz = q.size(),循环 sz 次 pop + 收集 + push children。这是层序所有变体的基础。

步骤

  1. 根节点入队,res 存结果
  2. while (!q.empty()):sz = q.size(),vector<int> level
  3. 循环 sz 次:auto node = q.front(); q.pop(); level.push_back(node->val)
  4. 若 node->left,入队;若 node->right,入队
  5. res.push_back(level),继续外层循环

C++ 实现

vector<vector<int>> levelOrder(TreeNode* root) {
    if (!root) return {};
    vector<vector<int>> res;
    queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        int sz = q.size();             // snapshot BEFORE inner loop
        vector<int> level;
        for (int i = 0; i < sz; i++) {
            auto 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;
}

层序变体

锯齿形层序(LC 103)
用 level 标志决定是否 reverse 当前层
右视图(LC 199)
每层只取最后一个节点
每层平均值(LC 637)
累加层总和除以 sz
最小深度(LC 111)
BFS 找第一个叶子节点时的层数,比 DFS 更高效

常见错误

忘记记录 sz必须在处理前记录 q.size(),否则新入队的子节点会混入当前层
空树处理root 为 null 时直接返回空 vector,不要入队
练习:二叉树的层序遍历代码练习
测试用例(4 个)
用例 1
输入:3 9 20 null null 15 7
期望:3 9 20 15 7
用例 2
输入:1
期望:1
用例 3
输入:1 2 3
期望:1 2 3
用例 4
输入:1 2 3 4 5 null 6
期望:1 2 3 4 5 6

BST 性质与验证

BST(二叉搜索树)性质:左子树所有节点 < 根 < 右子树所有节点。中序遍历结果严格递增。

核心性质

中序 = 升序BST 中序遍历得到严格递增序列,可用于验证、第 K 小元素
搜索 O(h)类比二分,每次排除一半子树;平衡 BST(AVL/红黑树)h = O(log n)
插入/删除找到位置后修改指针;删除有子节点时用中序后继替换

验证 BST:范围传递法

每个节点必须满足 lo < node->val < hi。递归传递范围:向左子树传 (lo, node->val),向右子树传 (node->val, hi)。初始 lo = LLONG_MIN, hi = LLONG_MAX,防止 INT_MIN/INT_MAX 的边界问题。

步骤

  1. 函数签名:bool isValidBST(TreeNode* root, long long lo, long long hi)
  2. base case:root 为 null 返回 true
  3. 若 root->val <= lo 或 root->val >= hi,返回 false
  4. 递归:isValidBST(left, lo, root->val) && isValidBST(right, root->val, hi)

C++ 实现

// Range propagation: every node must satisfy lo < val < hi
bool isValidBST(TreeNode* root,
                long long lo = LLONG_MIN,
                long long hi = LLONG_MAX) {
    if (!root) return true;
    if (root->val <= lo || root->val >= hi) return false;
    return isValidBST(root->left,  lo,          root->val)
        && isValidBST(root->right, root->val,   hi);
}

常见错误

只比较父节点错误做法:只检查 left->val < root->val。必须传递范围确保整棵子树合法
用 int 传边界节点值可能等于 INT_MIN/INT_MAX,边界应用 long long(LLONG_MIN/LLONG_MAX)
中序法的陷阱用 prev 记录中序前驱判断,同样需要 long long 初始化为 LLONG_MIN
练习:验证二叉搜索树代码练习
测试用例(5 个)
用例 1
输入:2 1 3
期望:true
用例 2
输入:5 1 4 null null 3 6
期望:false
用例 3
输入:1 2 3
期望:false
用例 4
输入:5 3 7 1 4 6 8
期望:true
用例 5
输入:2 2 2
期望:false

路径问题与树形 DP

路径类问题(最大路径和、直径)需要「后序思维」:先从子树收集信息,再在当前节点合并计算答案并向上传递。

最大路径和(LC 124)

关键:函数返回「经过 root 向上的最长单侧路径」(只能选左或右一侧),但同时在内部用 left + root + right 更新全局答案。注意路径值可能为负,取 max(arm, 0) 处理负子树。

  1. 定义 dfs(node) → 以 node 为端点向上的最长路径长(含 node->val)
  2. left = max(dfs(node->left), 0);right = max(dfs(node->right), 0)
  3. ans = max(ans, node->val + left + right)(当前节点作为路径最高点)
  4. 返回 node->val + max(left, right)(向上只能选一侧)

二叉树直径(LC 543)

直径 = 任意两节点间最长路径经过的边数。等价于找 left_depth + right_depth 的最大值(不含根节点处的额外计数)。与最大路径和同构,把路径「和」换成「深度」即可。

最近公共祖先(LC 236)

递归:若 root 为 null 或等于 p/q,返回 root。分别在左右子树找 p 和 q;若左右各找到一个,则 root 即为 LCA;若只有一侧找到,返回那一侧(说明两个节点都在同一侧)。

C++ 参考实现

int ans = INT_MIN;

// Returns the longest single-arm path through root going upward
int dfs(TreeNode* root) {
    if (!root) return 0;
    int left  = max(dfs(root->left),  0); // discard negative arms
    int right = max(dfs(root->right), 0);
    ans = max(ans, root->val + left + right); // root as path peak
    return root->val + max(left, right);      // pass up best single arm
}

// LC 124 — Binary Tree Maximum Path Sum
int maxPathSum(TreeNode* root) {
    ans = INT_MIN;
    dfs(root);
    return ans;
}
// LC 236 — Lowest Common Ancestor (general binary tree)
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
    if (!root || root == p || root == q) return root;
    auto left  = lowestCommonAncestor(root->left,  p, q);
    auto right = lowestCommonAncestor(root->right, p, q);
    if (left && right) return root; // p and q on opposite sides
    return left ? left : right;     // both on the same side
}

常见错误

路径可为负处理 left/right arm 时用 max(arm, 0),否则负子树会降低全局答案
返回值 vs 全局答案dfs 返回的是「单侧路径」给父节点用;全局 ans 在内部用「双侧路径」更新,两者不同
直径 vs 深度直径用边数(= 两侧深度之和),不是节点数(= 两侧深度之和 + 1)

LeetCode 题型

按技术分类的高频二叉树题目。

遍历

144/94/145. 前/中/后序遍历
递归版 3 行;迭代版中序是重点
102. 二叉树的层序遍历
BFS 模板,记录 sz 分层
105. 从前序与中序构造二叉树
前序首元素是根,在中序中找根位置分左右

BST

98. 验证二叉搜索树
范围传递法
230. BST 中第 K 小的元素
中序遍历计数
235/236. 最近公共祖先
235 利用 BST 性质;236 通用 LCA
450. 删除 BST 中的节点
找中序后继替换

路径与树形 DP

104. 二叉树的最大深度
递归 1 + max(l, r)
543. 二叉树的直径
后序,更新 left+right 最大值
124. 二叉树中的最大路径和
后序,max(arm,0) + 全局 ans
337. 打家劫舍 III
树形 DP,每节点返回 [偷/不偷] 两种状态的最大值

构造与序列化

226. 翻转二叉树
后序,swap(left, right)
297. 二叉树的序列化与反序列化
BFS 或前序递归
114. 二叉树展开为链表
后序:先展开左右,再拼接
116. 填充每个节点的下一个右侧节点
层序或递归利用 next 指针