最大二叉树 II 图解题解
这道题到底在问什么
- 输入
- a = 30,80,20,60,val = 70
- 输出
- 80 的右孩子由 60 变成 70,60 子树挂到 70 左边
最优解:为什么这么做
一句话答案:LeetCode 998 最大二叉树 II:val 追加在数组末尾,只会落到根的右链上——沿右链下行,遇到比它小的节点就取代它、原子树收作左孩子,时间 O(h)、最坏 O(n)。
b 建成的最大树,和原来那棵差在哪
最大树的规矩是:每个节点都比自己子树里的所有数大,所以整棵树里最大的数坐在根上。原树是数组 a 用 Construct 建的,即取区间最大值当根,最大值左边的数建左子树、右边的数建右子树。现在把 val 追加到 a 的末尾得到 b,要你返回 b 重新 Construct 出来的最大树。题面给的是 a=30,80,20,60、val=70。
把数组还原重新 Construct,为什么白费力气
数组末尾多了个 val,一种做法是把 a 拼上 val、整个数组从头 Construct 一遍。可原树已经建好摆在手里,全部重建至少 O(n) 起步,而这趟里绝大多数节点的位置压根不动:追加一个末尾元素,只会撼动树里很小的一段,为它重建整棵树纯属浪费。
新值排在末尾,凭什么只能进右链
回到 Construct 的定义:一个节点的左子树由它前面的元素建、右子树由它后面的元素建。val 放在数组最末尾,排在所有现存元素后面,所以无论哪一层,它都只可能落在某个节点右侧。于是左子树那一整边完全不用碰,只要从根出发、顺着那条一路向右的右链往下找它的落脚点就行。
沿右链下行,遇到比它小的就让位
下行时每到一个节点就拿它和 val 比一次。若当前节点比 val 小,说明 val 该取代它当这一段的新根,于是新建一个值为 val 的节点,把当前节点连着下面整棵子树收作它的左孩子,右孩子留空;原子树里的值都比 val 小,正好待在左边,规矩没破。若当前节点比 val 大,val 还不够格,就钻进它的右子树接着比。写成递归就是:root 为空或 root < val 时返回 TreeNode(val, root),否则让 root.right 接上右子树的递归结果、返回 root。
a=30,80,20,60 追加 70,右链上走两步
原树里 80 当根,左孩子 30,右孩子 60,60 底下挂着 20,右链就是 80 到 60 这一串。从根 80 起:70 < 80,80 稳坐不动,滑到它的右孩子 60。再比:60 < 70,插入点就定在这儿。新建节点 70,把 60 连着 20 这整棵收作 70 的左孩子,70 的右孩子留空,再把 80 原来指向 60 的右指针改指 70。结果:80 还是根,左边 30 没动,右边由 60 换成 70,70 底下挂着 60 和 20,满足 80 > 70 > 60 > 20。全程只碰了右链上的 80 和 60 两个节点。
比较写反、或把原子树丢了,结果全乱
遇到比 val 小的节点让位时,别把它原来的子树顺手丢了,原子树的值都小于 val,得整棵收进它左边,丢一棵就漏一批数据。比较方向也别写反:只有节点 < val 才让位,写成节点比 val 大才插入,插出来的树整个是错的。复杂度上,只沿右链走一趟,时间是右链长度 O(h);树退化成一路向右的偏链时右链长 n,最坏 O(n);递归栈也只在右链上展开,空间同样是 O(h)、最坏 O(n)。另外,val 比根还大时,在根处就让位、原树整棵成了它的左孩子;val 比所有节点都小时,一路走到右链末端、当最右节点的右孩子。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这句口诀:追加在末尾,只走右链;比 val 小就让位、比 val 大就继续往右。后面每帧都在套它。
- 4根 80 最大这是一棵最大树。它的规矩是:每个节点都比自己子树里的所有节点大。所以最大的 80 坐在根上。
- 580 > 子树一切验证一下:80 比它子树里的 30、60、20 全都大,所以它才有资格当根。这就是最大树最核心的那条规矩。
- 660 > 20这条规矩一层层往下都成立:右边的 60 也比它子树里的 20 大。每一棵子树都自带同一条规矩。
- 7a = 30, 80, 20, 60这棵树是从数组 a = 30, 80, 20, 60 用 Construct 建出来的:每次取区间最大值当根,左边的数建左子树,右边的数建右子树。先把数组的样子记牢。
- 8b = a + 70现在来了新值 70,题目要求把它追加到数组 a 的最末尾,得到 b = 30, 80, 20, 60, 70。我们要的是 b 重新建成的最大树。
- 9锁定右链 80 → 60关键一步想通就赢了:70 排在数组最末尾,在 Construct 里它永远排在所有现存元素后面,所以只可能挂在某个节点的右边。我们只需沿着根的右链往下找,右链就是 80 到 60 这一串。
- 10左子树不动左子树 30 这一边完全不用碰。因为 70 在末尾,绝不会跑到现存元素的左边去,注意力全部放在右链上。
- 1170 与 80 比从根 80 开始下行。拿 70 和 80 比一比:70 能不能比 80 还大、从而取代它当新根?
- 1270 < 8070 比 80 小,所以 80 稳坐根位不动。70 只能沉进 80 的右子树继续找位置,指针准备滑向它的右孩子。
- 13指针到 60指针顺着 80 的右指针滑到下一个节点 60,继续往右链深处找它的位置。
- 1470 与 60 比现在指针来到右孩子 60。再比一次:70 和 60 谁大?
- 1570 > 6070 比 60 大,插入点就定在这儿!按最大树的规矩,70 要当这一段的新根,原来的 60 得给它让位。
- 1660 子树整体下移要注意:让位的是 60 连着它下面的 20 这一整棵子树,谁都不能丢。下一步把这整棵挂到新节点 70 的左边。
- 1770 占 60 原位新建一个节点 70,放到 60 原来的位置上,也就是接在 80 的右边。
- 1860 子树 → 70 的左刚才让位的 60 加上 20 这一整棵,挂到 70 的左孩子上。因为它们都比 70 小,正好待在 70 的左子树里,规矩一点没破。
- 1970 右孩子空70 的右孩子是空的。道理也简单:70 是数组最后一个元素,它后面再没有别的数,右边自然空着。
- 2080 的右 = 70最后把 80 原来指向 60 的那根右指针,改成指向 70。这样新节点 70 就正式接回了整棵树。
- 21新树成形新的最大树成形了。80 还是根,左边的 30 没动,右边由 60 换成了 70,70 底下挂着 60 和 20。
- 2280 > 70验证一下规矩有没有破。先看父子:80 > 70,根依然是最大,这一关过了。
- 2370 > 60 > 20再看 70 这棵子树:70 > 60 > 20,每个节点都比自己子树里的大,完全满足最大树的规矩。
- 24答案 = 新树回放整个过程:从根 80 沿右链下行,70 一路比下来,到 60 时终于更大,就在那里取代 60、把 60 子树收作左孩子。一次右链下行,插入完成。
- 25只走右链 O(h)整趟只碰了右链上的 80 和 60 两个节点,没有重建任何子树,这就是增量插入的高效之处。
⚠️ 容易写错的地方
✗ 错:把整个数组还原出来重新 Construct
✓ 对:只在原树上沿右链增量插入
重建是 O(n) 起步且没必要,末尾追加只影响右链
✗ 错:val 比节点大时,直接丢掉原来的子树
✓ 对:原子树要整棵收作新节点的左孩子
原子树的值都比 val 小,必须保留在 val 的左边,丢了就漏数据
✗ 错:跑到左子树里去找插入位置
✓ 对:只可能在右链上
val 追加在末尾,在 Construct 里永远排在现存元素右侧
✗ 错:比较方向写反(node 比 val 大就插入)
✓ 对:应是 node 比 val 小才让位
只有遇到比 val 小的节点,val 才该取代它当新根
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def insertIntoMaxTree(
self, root: Optional[TreeNode], val: int
) -> Optional[TreeNode]:
if root is None or root.val < val:
return TreeNode(val, root)
root.right = self.insertIntoMaxTree(root.right, val)
return rootC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
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 *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
/**
* Definition for a binary tree node.
* 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 *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* insertIntoMaxTree(TreeNode* root, int val) {
if (!root || root->val < val) return new TreeNode(val, root, nullptr);
root->right = insertIntoMaxTree(root->right, val);
return root;
}
};Java
import java.util.*;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public TreeNode insertIntoMaxTree(TreeNode root, int val) {
if (root == null || root.val < val) {
return new TreeNode(val, root, null);
}
root.right = insertIntoMaxTree(root.right, val);
return root;
}
}复杂度
时间
O(h),最坏 O(n)
h 为根的右链长度,只沿右链下行一次;退化成右偏链时右链长 n,故最坏 O(n)
空间
O(h),最坏 O(n)
递归只在右链上展开,栈深等于右链长度;改写成循环可降到 O(1) 额外空间
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最大二叉树 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么追加到末尾就只影响右链,能不能说严谨点?+
在 Construct 里,一个节点的左子树由它前面的元素建、右子树由它后面的元素建。val 排在数组最末尾,就排在所有现存元素之后,所以不管在哪一层,它都只可能落到某个节点的右子树那一侧。沿根的右链一路比下去,要么在某处比当前节点大、就地取代它,要么走到右链末端、成为最右节点的右孩子,两种落点都在右链上,左子树自始至终不会被动到。
这道题和「最大二叉树 I」从数组建树是什么关系?+
I 是从零把整个数组建成最大树,常见做法是分治 O(n²) 或单调栈 O(n)。II 是在已经建好的最大树上,增量插入一个追加到末尾的元素,只沿右链走 O(h)。可以把 II 看成 I 的在线增量版:已有的树不推倒重来,只补上新元素该落的那个位置。
不用递归,改成循环行不行?+
行。递归本质只是沿右链一路向右,换成 while 循环、拿一个指针顺着右指针往下走,找到第一个比 val 小的节点(或走到空)就停,同样是 O(h)。好处是省掉递归栈,额外空间从 O(h) 降到 O(1)。接线要小心:让位那步把原节点整棵挂到新节点左边,再把上一个节点的右指针改指新节点;若插入点正好在根,新节点直接当根返回。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最大二叉树 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。