二叉树专题(下)

二叉树专题(下)------ 进阶操作

本篇覆盖二叉树专题的后 7 题:BST 中第 K 小的元素、二叉树的右视图、二叉树展开为链表、从前序与中序遍历序列构造二叉树、路径总和 III、二叉树的最近公共祖先、二叉树中的最大路径和。这部分题目综合性更强,需要对遍历框架有更灵活的运用。


一、二叉搜索树中第 K 小的元素(#230)

题意

给一棵 BST 的根节点和整数 k,返回 BST 中第 k 小的元素。

复制代码
    3
   / \
  1   4
   \
    2

k=1,输出:1
k=3,输出:3

思路

BST 的中序遍历结果是升序序列,第 k 小的元素就是中序遍历的第 k 个节点。

不需要把中序结果全部存下来,用一个计数器 count,中序遍历过程中每访问一个节点就 count--,减到 0 时记录答案并停止。

复制代码
中序遍历:1 → 2 → 3 → 4
k=3,count从3开始
访问1:count=2
访问2:count=1
访问3:count=0 → 记录答案3,停止

代码

cpp 复制代码
class Solution {
    int count, res;
public:
    void dfs(TreeNode* root) {
        if (!root || count == 0) return;
        dfs(root->left);
        count--;
        if (count == 0) { res = root->val; return; }
        dfs(root->right);
    }

    int kthSmallest(TreeNode* root, int k) {
        count = k;
        dfs(root);
        return res;
    }
};

复杂度

  • 时间 :O(H+k)O(H + k)O(H+k),HHH 为树高,最坏 O(n)O(n)O(n)
  • 空间 :O(H)O(H)O(H),递归栈深度

二、二叉树的右视图(#199)

题意

给二叉树根节点,想象站在树的右侧,返回从上到下每一层能看到的节点值(即每层最右边的节点)。

复制代码
    1            ← 看到 1
   / \
  2   3          ← 看到 3
   \   \
    5   4        ← 看到 4

输出:[1, 3, 4]

思路

层序遍历,每层的最后一个节点就是右视图能看到的节点。

复用上篇层序遍历的框架,每层遍历结束时把最后一个节点的值加入结果。

复制代码
第1层:[1],最右=1
第2层:[2,3],最右=3
第3层:[5,4],最右=4

结果:[1,3,4]

代码

cpp 复制代码
class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        if (!root) return {};
        vector<int> res;
        queue<TreeNode*> q;
        q.push(root);
        while (!q.empty()) {
            int size = q.size();
            for (int i = 0; i < size; i++) {
                TreeNode* node = q.front(); q.pop();
                if (i == size - 1) res.push_back(node->val); // 每层最后一个
                if (node->left) q.push(node->left);
                if (node->right) q.push(node->right);
            }
        }
        return res;
    }
};

复杂度

  • 时间 :O(n)O(n)O(n)
  • 空间 :O(n)O(n)O(n)

三、二叉树展开为链表(#114)

题意

给二叉树根节点,将其展开为链表(原地),展开后链表的顺序与前序遍历一致,用 right 指针连接,left 指针全部置为 null

复制代码
    1                1
   / \                \
  2   5      →        2
 / \   \               \
3   4   6               3
                         \
                          4
                           \
                            5
                             \
                              6

思路

前序遍历顺序:根 → 左 → 右。展开后,每个节点的 right 指向前序遍历的下一个节点。

后序思路:先递归展开左子树和右子树,再把左子树接到根的右边,把原来的右子树接到左子树展开后的末尾。

复制代码
以节点1为例,假设左右子树已经展开:
  左链表:2→3→4
  右链表:5→6

操作:
  1. 找左链表的末尾节点(4)
  2. 把原右链表(5→6)接到末尾:4->right = 5→6
  3. 把左链表接到根右边:1->right = 2→3→4→5→6
  4. 左指针置空:1->left = null

结果:1→2→3→4→5→6

代码

cpp 复制代码
class Solution {
public:
    void flatten(TreeNode* root) {
        if (!root) return;
        flatten(root->left);
        flatten(root->right);

        // 此时左右子树都已展开为链表
        TreeNode* left = root->left;
        TreeNode* right = root->right;

        if (!left) return; // 没有左子树,不需要操作

        // 找左链表的末尾
        TreeNode* tail = left;
        while (tail->right) tail = tail->right;

        // 把右链表接到左链表末尾
        tail->right = right;
        // 把左链表接到根的右边
        root->right = left;
        root->left = nullptr;
    }
};

复杂度

  • 时间 :O(n)O(n)O(n),每个节点访问一次,找末尾节点的总步数也是 O(n)O(n)O(n)
  • 空间 :O(n)O(n)O(n),递归栈

四、从前序与中序遍历序列构造二叉树(#105)

题意

给二叉树的前序遍历序列 preorder 和中序遍历序列 inorder,构造并返回二叉树。

复制代码
preorder = [3, 9, 20, 15, 7]
inorder  = [9, 3, 15, 20, 7]

构造结果:
    3
   / \
  9  20
    /  \
   15   7

思路

前序遍历的第一个元素一定是根节点。

找到根节点值在中序遍历中的位置 idxidx 左边是左子树的中序序列(长度为 leftSize),右边是右子树的中序序列。

根据 leftSize,在前序遍历中划分出左子树和右子树的前序序列,递归构造。

复制代码
preorder = [3, 9, 20, 15, 7]
inorder  = [9, 3, 15, 20, 7]

根节点 = preorder[0] = 3
3 在 inorder 中的位置 idx=1
左子树大小 leftSize = 1

左子树:
  前序:preorder[1..1] = [9]
  中序:inorder[0..0]  = [9]
  → 根=9,无子节点

右子树:
  前序:preorder[2..4] = [20,15,7]
  中序:inorder[2..4]  = [15,20,7]
  → 根=20,左子=15,右子=7

用哈希表存中序遍历每个值的下标,查找 idx 时 O(1)O(1)O(1)。

代码

cpp 复制代码
class Solution {
    unordered_map<int, int> indexMap; // val → 中序下标

    TreeNode* build(vector<int>& preorder, int preLeft, int preRight,
                    vector<int>& inorder, int inLeft, int inRight) {
        if (preLeft > preRight) return nullptr;
        int rootVal = preorder[preLeft];
        int idx = indexMap[rootVal];       // 根在中序中的位置
        int leftSize = idx - inLeft;       // 左子树节点数

        TreeNode* root = new TreeNode(rootVal);
        root->left = build(preorder, preLeft + 1, preLeft + leftSize,
                           inorder, inLeft, idx - 1);
        root->right = build(preorder, preLeft + leftSize + 1, preRight,
                            inorder, idx + 1, inRight);
        return root;
    }

public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        for (int i = 0; i < inorder.size(); i++)
            indexMap[inorder[i]] = i;
        return build(preorder, 0, preorder.size() - 1,
                     inorder, 0, inorder.size() - 1);
    }
};

复杂度

  • 时间 :O(n)O(n)O(n),每个节点创建一次,哈希查找 O(1)O(1)O(1)
  • 空间 :O(n)O(n)O(n),哈希表 + 递归栈

五、路径总和 III(#437)

题意

给二叉树根节点和整数 targetSum,返回路径和等于 targetSum 的路径数目。路径方向向下(从父节点到子节点),但不需要从根节点开始,也不需要在叶节点结束。

复制代码
        10
       /  \
      5   -3
     / \    \
    3   2   11
   / \   \
  3  -2   1

targetSum = 8

满足条件的路径:
  5→3     = 8
  5→2→1   = 8
  -3→11   = 8

输出:3

思路

暴力做法:对每个节点作为起点,向下搜索所有路径,O(n2)O(n^2)O(n2)。

前缀和 + 哈希表 ,O(n)O(n)O(n)。

和「子数组和为 K」一题思路完全一致,只是从数组换到了树上。

定义从根节点到当前节点的路径和为 prefix,则以当前节点为终点、路径和为 targetSum 的路径数,等于之前路径中前缀和为 prefix - targetSum 的数量。

unordered_map<long, int> cnt 记录根到当前节点路径上所有前缀和的出现次数,DFS 过程中:

  • 进入节点:查询 cnt[prefix - targetSum],累加到答案;然后把当前 prefix 加入 cnt

  • 离开节点(回溯):把当前 prefixcnt 中移除,恢复现场

    cnt 初始化:{0: 1}(空路径前缀和为0)

    进入10:prefix=10,查 10-8=2,cnt里无 → ans+=0,cnt={0:1, 10:1}
    进入5:prefix=15,查 15-8=7,cnt里无 → ans+=0,cnt={...,15:1}
    进入3:prefix=18,查 18-8=10,cnt里有1 → ans+=1(路径5→3)
    ...
    离开5:cnt中移除15

代码

cpp 复制代码
class Solution {
    unordered_map<long, int> cnt;
    int target;

    int dfs(TreeNode* root, long prefix) {
        if (!root) return 0;
        prefix += root->val;
        int res = cnt.count(prefix - target) ? cnt[prefix - target] : 0;
        cnt[prefix]++;
        res += dfs(root->left, prefix);
        res += dfs(root->right, prefix);
        cnt[prefix]--; // 回溯,恢复现场
        return res;
    }

public:
    int pathSum(TreeNode* root, int targetSum) {
        cnt[0] = 1; // 空路径
        target = targetSum;
        return dfs(root, 0);
    }
};

复杂度

  • 时间 :O(n)O(n)O(n),每个节点访问一次
  • 空间 :O(n)O(n)O(n),哈希表 + 递归栈

六、二叉树的最近公共祖先(#236)

题意

给二叉树根节点和两个节点 pq,找到它们的最近公共祖先(LCA)。最近公共祖先定义为:能同时是 pq 祖先的最深节点(节点也可以是自身的祖先)。

复制代码
        3
       / \
      5   1
     / \ / \
    6  2 0  8
      / \
     7   4

p=5, q=1 → LCA=3
p=5, q=4 → LCA=5

思路

后序遍历,递归函数的返回值含义:

  • 如果当前节点是 pq,返回当前节点
  • 如果当前节点为空,返回 null
  • 递归处理左右子树,分别得到 leftright
    • leftright 都不为空:说明 pq 分别在左右子树,当前节点就是 LCA

    • 只有 left 不为空:pq 都在左子树,返回 left

    • 只有 right 不为空:pq 都在右子树,返回 right

      p=5, q=4

      递归到节点5:5==p,直接返回5(不继续递归)
      递归到节点1:左右子树都找不到p或q,返回null
      节点3处:left=5(非空),right=null → 返回left=5?

      等等,p=5, q=4,4是5的子节点。
      递归到5时直接返回5,向上传递。
      节点3处:left=5,right=null → 返回5。

      这意味着5就是LCA,正确。
      原因:找到p(5)后直接返回,不继续向下找q(4),
      但能保证q一定在p的子树里(题目保证p,q都存在),
      所以p就是它们的LCA。

代码

cpp 复制代码
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (!root || root == p || root == q) return root;
        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);
        if (left && right) return root; // p、q分别在左右子树
        return left ? left : right;     // 返回非空的那个
    }
};

复杂度

  • 时间 :O(n)O(n)O(n)
  • 空间 :O(n)O(n)O(n)

七、二叉树中的最大路径和(#124)

题意

给二叉树根节点,找路径(不需要经过根节点,方向任意)中节点值之和最大的路径,返回该最大路径和。节点值可以为负数。

复制代码
    -10
    /  \
   9   20
      /  \
     15   7

最大路径:15→20→7,路径和=42

思路

和「二叉树直径」思路几乎完全一致,只是把"路径长度(边数)"换成了"路径节点值之和"。

定义辅助函数 maxGain(node):返回以 node 为起点向下延伸的路径中,能贡献的最大值(如果最大值为负,取 0,即不选这条路)。

maxGain(node)=node.val+max⁡(maxGain(left), maxGain(right), 0)maxGain(node) = node.val + \max(maxGain(left),\ maxGain(right),\ 0)maxGain(node)=node.val+max(maxGain(left), maxGain(right), 0)

以每个节点为"转折点"(路径最高点)时,路径和为:

node.val+max⁡(maxGain(left), 0)+max⁡(maxGain(right), 0)node.val + \max(maxGain(left),\ 0) + \max(maxGain(right),\ 0)node.val+max(maxGain(left), 0)+max(maxGain(right), 0)

后序遍历,在计算每个节点 maxGain 的同时更新全局最大路径和。

复制代码
节点15:maxGain=15,以15为转折点路径和=15
节点7:maxGain=7,以7为转折点路径和=7
节点20:
  left_gain = max(15,0) = 15
  right_gain = max(7,0) = 7
  以20为转折点:20+15+7=42 → 更新res=42
  maxGain(20) = 20+max(15,7) = 35

节点9:maxGain=9,以9为转折点路径和=9
节点-10:
  left_gain = max(9,0) = 9
  right_gain = max(35,0) = 35
  以-10为转折点:-10+9+35=34 < 42,不更新res
  maxGain(-10) = -10+max(9,35) = 25

最终res=42

代码

cpp 复制代码
class Solution {
    int res = INT_MIN;
public:
    int maxGain(TreeNode* root) {
        if (!root) return 0;
        int left = max(maxGain(root->left), 0);   // 负收益不选
        int right = max(maxGain(root->right), 0);
        res = max(res, root->val + left + right);  // 以当前节点为转折点
        return root->val + max(left, right);        // 向上只能选一侧
    }

    int maxPathSum(TreeNode* root) {
        maxGain(root);
        return res;
    }
};

注意 res 初始化为 INT_MIN 而不是 0,因为节点值可以全为负数,路径必须至少包含一个节点。

复杂度

  • 时间 :O(n)O(n)O(n)
  • 空间 :O(n)O(n)O(n)

八、本篇小结

题目 遍历方式 核心思路 时间 空间
BST 第 K 小 中序 中序第 k 个节点即为答案 O(H+k)O(H+k)O(H+k) O(H)O(H)O(H)
右视图 BFS 层序遍历取每层最后一个节点 O(n)O(n)O(n) O(n)O(n)O(n)
展开为链表 后序 先展开子树,再拼接 O(n)O(n)O(n) O(n)O(n)O(n)
前中序构造二叉树 前序 前序首元素为根,中序划分左右子树 O(n)O(n)O(n) O(n)O(n)O(n)
路径总和 III 前序+回溯 前缀和+哈希表,回溯恢复现场 O(n)O(n)O(n) O(n)O(n)O(n)
最近公共祖先 后序 左右子树分别找,都找到则当前节点为LCA O(n)O(n)O(n) O(n)O(n)O(n)
最大路径和 后序 每个节点为转折点,负收益不选 O(n)O(n)O(n) O(n)O(n)O(n)

二叉树专题到此全部结束。纵观 15 道题,后序遍历出现频率最高,原因是后序能拿到左右子树的计算结果,适合"自底向上汇总信息"的场景。路径总和 III 和最大路径和是本专题难度最高的两道,前者把前缀和移植到树上,后者需要清楚区分"向上返回的值"和"更新答案时用的值"这两个不同的量。

相关推荐
故事和你911 小时前
洛谷-数据结构2-1-二叉堆与树状数组1
开发语言·数据结构·c++·算法·动态规划·图论
多加点辣也没关系2 小时前
数据结构与算法|第十七章:贪心算法
数据结构·算法·贪心算法
多加点辣也没关系2 小时前
数据结构与算法|第十四章:排序算法(上)— 比较类排序
数据结构·算法·排序算法
笨笨饿2 小时前
#72_聊聊I2C以及他们的变体
linux·c语言·网络·stm32·单片机·算法·个人开发
机器人图像处理2 小时前
6-自动白平衡(灰度世界算法)
opencv·算法·相机
Dr.Zeus2 小时前
从电芯到系统:BMS算法视角下的电池热管理深度解析作者署名
算法·能源
ulias2122 小时前
leetcode热题 - 6
linux·算法·leetcode
七颗糖很甜2 小时前
卫星通信遇到“太空天气”会怎样---电离层闪烁对卫星通信的影响
大数据·python·算法
小凡子空白在线学习2 小时前
工作拆分so总结
java·jvm·算法