百日算法修炼 · Day 15
主题:二叉树递归套路、树形 DP、完全二叉树、最近公共祖先、树上最大距离、员工快乐值
语言:Java | 建议用时:120~180 分钟
阅读方式
本文分为两个区域:
- 上半篇是独立练习区:只有题目、示例、思考问题和可折叠提示;
- 下半篇是参考答案区:包含优化后的代码、过程图解、递归信息设计、复杂度、易错点与验证思路。
建议先完成全部题目,再通过"查看参考答案"链接跳转。
Day 15 的主线不是背 8 道题,而是掌握一个可以反复迁移的套路:
向左子树要信息,向右子树要信息,再用左右信息加工出当前节点的信息。
这就是二叉树题里非常重要的 递归信息模型 / 树形 DP。
今日任务地图
#mermaid-svg-BSH5b8gIEQZHr2Ct{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BSH5b8gIEQZHr2Ct .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BSH5b8gIEQZHr2Ct .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BSH5b8gIEQZHr2Ct .error-icon{fill:#552222;}#mermaid-svg-BSH5b8gIEQZHr2Ct .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BSH5b8gIEQZHr2Ct .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BSH5b8gIEQZHr2Ct .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BSH5b8gIEQZHr2Ct .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BSH5b8gIEQZHr2Ct .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BSH5b8gIEQZHr2Ct .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BSH5b8gIEQZHr2Ct .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BSH5b8gIEQZHr2Ct .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BSH5b8gIEQZHr2Ct .marker.cross{stroke:#333333;}#mermaid-svg-BSH5b8gIEQZHr2Ct svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BSH5b8gIEQZHr2Ct p{margin:0;}#mermaid-svg-BSH5b8gIEQZHr2Ct .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BSH5b8gIEQZHr2Ct .cluster-label text{fill:#333;}#mermaid-svg-BSH5b8gIEQZHr2Ct .cluster-label span{color:#333;}#mermaid-svg-BSH5b8gIEQZHr2Ct .cluster-label span p{background-color:transparent;}#mermaid-svg-BSH5b8gIEQZHr2Ct .label text,#mermaid-svg-BSH5b8gIEQZHr2Ct span{fill:#333;color:#333;}#mermaid-svg-BSH5b8gIEQZHr2Ct .node rect,#mermaid-svg-BSH5b8gIEQZHr2Ct .node circle,#mermaid-svg-BSH5b8gIEQZHr2Ct .node ellipse,#mermaid-svg-BSH5b8gIEQZHr2Ct .node polygon,#mermaid-svg-BSH5b8gIEQZHr2Ct .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BSH5b8gIEQZHr2Ct .rough-node .label text,#mermaid-svg-BSH5b8gIEQZHr2Ct .node .label text,#mermaid-svg-BSH5b8gIEQZHr2Ct .image-shape .label,#mermaid-svg-BSH5b8gIEQZHr2Ct .icon-shape .label{text-anchor:middle;}#mermaid-svg-BSH5b8gIEQZHr2Ct .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BSH5b8gIEQZHr2Ct .rough-node .label,#mermaid-svg-BSH5b8gIEQZHr2Ct .node .label,#mermaid-svg-BSH5b8gIEQZHr2Ct .image-shape .label,#mermaid-svg-BSH5b8gIEQZHr2Ct .icon-shape .label{text-align:center;}#mermaid-svg-BSH5b8gIEQZHr2Ct .node.clickable{cursor:pointer;}#mermaid-svg-BSH5b8gIEQZHr2Ct .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BSH5b8gIEQZHr2Ct .arrowheadPath{fill:#333333;}#mermaid-svg-BSH5b8gIEQZHr2Ct .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BSH5b8gIEQZHr2Ct .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BSH5b8gIEQZHr2Ct .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BSH5b8gIEQZHr2Ct .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BSH5b8gIEQZHr2Ct .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BSH5b8gIEQZHr2Ct .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BSH5b8gIEQZHr2Ct .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BSH5b8gIEQZHr2Ct .cluster text{fill:#333;}#mermaid-svg-BSH5b8gIEQZHr2Ct .cluster span{color:#333;}#mermaid-svg-BSH5b8gIEQZHr2Ct div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BSH5b8gIEQZHr2Ct .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BSH5b8gIEQZHr2Ct rect.text{fill:none;stroke-width:0;}#mermaid-svg-BSH5b8gIEQZHr2Ct .icon-shape,#mermaid-svg-BSH5b8gIEQZHr2Ct .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BSH5b8gIEQZHr2Ct .icon-shape p,#mermaid-svg-BSH5b8gIEQZHr2Ct .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BSH5b8gIEQZHr2Ct .icon-shape .label rect,#mermaid-svg-BSH5b8gIEQZHr2Ct .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BSH5b8gIEQZHr2Ct .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BSH5b8gIEQZHr2Ct .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BSH5b8gIEQZHr2Ct :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始 Day 15
建立递归信息模型
平衡二叉树
满二叉树
二叉搜索树
最大搜索二叉子树
完全二叉树
最近公共祖先
树上最大距离
多叉树最大快乐值
统一复盘树形 DP
| 题号 | 主题 | 核心能力 |
|---|---|---|
| 1 | 平衡二叉树 | 高度 + 子树状态 |
| 2 | 满二叉树 | 完美结构的递归定义 |
| 3 | 二叉搜索树 | min / max / isBST 信息 |
| 4 | 最大搜索二叉子树 | "整棵成立"与"局部最优"同时维护 |
| 5 | 完全二叉树 | 层序规则 + 递归分类讨论 |
| 6 | 最近公共祖先 | 节点是否出现 + 答案上传 |
| 7 | 二叉树最大距离 | 高度 + 穿过当前节点的路径 |
| 8 | 派对最大快乐值 | 多叉树树形 DP:来 / 不来 |
第一部分:独立练习区
先别急着翻答案。每道题开始前都问自己:
"父节点为了做出判断,到底需要从左右子树拿到哪些信息?"
练习 1:判断平衡二叉树
给定一棵二叉树,判断它是否为平衡二叉树。
本文沿用本题代码约定:
text
root == null 时,对外返回 false
对于任意非空节点,平衡条件是:
text
1. 左子树平衡
2. 右子树平衡
3. 左右子树高度差不超过 1
思考问题
- 当前节点要判断自己是否平衡,需要向左右孩子获取哪些信息?
- 为什么只知道"左右子树是否平衡"还不够?
- 空树在递归内部为什么适合返回"平衡,高度 0"?
- 高度应该在什么时候计算?
- 能否在某一侧已经不平衡时提前结束递归?这样做和统一信息模型相比有什么取舍?
提示 1:父节点需要两个维度
父节点既要知道左右子树本身是否平衡,也要知道左右高度,才能判断当前节点的高度差。
提示 2:先相信子树已经解决
递归函数不需要关心左子树内部怎么判断,只要规定它能返回"是否平衡 + 高度"即可。
写完后再看:[查看练习 1 参考答案](#查看练习 1 参考答案)
练习 2:判断满二叉树
这里先统一术语。
本文把"满二叉树"定义为通常所说的 Perfect Binary Tree(完美二叉树):
每一层都被完全填满,所有叶子节点处于同一深度。
例如:
text
1
/ \
2 3
/ \ / \
4 5 6 7
是满二叉树。
而下面这棵树虽然每个非叶子节点都有两个孩子,但叶子不在同一层,因此不是本文定义的满二叉树:
text
1
/ \
2 3
/ \
4 5
思考问题
- 如果左右子树都是满二叉树,还缺少什么条件,才能保证当前整棵树也是满二叉树?
- 为什么"每个节点要么 0 个孩子,要么 2 个孩子"并不足以判断 Perfect Binary Tree?
- 如果递归返回
height和isFull,当前节点如何合并信息? - 空树作为递归基线时应该返回什么?
提示:满不满不仅看结构,还看高度
左右子树都必须是满二叉树,并且高度必须完全相同。
写完后再看:[查看练习 2 参考答案](#查看练习 2 参考答案)
练习 3:判断二叉搜索树
给定一棵二叉树,判断它是否为严格二叉搜索树。
本文采用严格定义:
text
左子树所有值 < 当前值 < 右子树所有值
因此不允许重复值跨越父节点边界。
思考问题
- 为什么只比较
root.left.value < root.value < root.right.value不够? - 当前节点需要知道左子树的哪个极值?
- 当前节点需要知道右子树的哪个极值?
- 除了
min和max,为什么还必须知道左右子树自身是不是 BST? - 空节点返回
null和返回哨兵信息各有什么优缺点?
提示:不要只盯着直接孩子
左子树里最"大"的节点也必须小于当前节点;右子树里最"小"的节点也必须大于当前节点。
写完后再看:[查看练习 3 参考答案](#查看练习 3 参考答案)
练习 4:最大搜索二叉子树的大小
给定一棵普通二叉树,返回其中节点数量最多的二叉搜索子树所包含的节点数。
例如:
text
6
/ \
1 12
/ \
10 13
/ \
4 14
整棵树不一定是 BST,但某个局部子树可能是 BST。
思考问题
- 父节点要判断"以自己为根的整棵子树是不是 BST",需要哪些信息?
- 如果当前整棵子树不是 BST,答案应该来自哪里?
- 如果当前整棵子树是 BST,它的大小应该如何计算?
- 为什么只保存
maxSubBSTSize还不够? - 是否需要额外保存"当前子树总节点数"?
提示 1:有两类答案来源
最大 BST 要么完全位于左子树,要么完全位于右子树,要么就是"以当前节点为根的整棵子树"。
提示 2:整棵成立时要算整棵大小
如果当前子树本身是 BST,答案不是"孩子答案 + 1",而是当前整棵子树的节点总数。
写完后再看:[查看练习 4 参考答案](#查看练习 4 参考答案)
练习 5:判断完全二叉树
完全二叉树(Complete Binary Tree)的直观规则:
除最后一层外,其余层必须填满;最后一层的节点必须从左向右连续出现。
例如:
text
1
/ \
2 3
/ \ /
4 5 6
是完全二叉树。
而:
text
1
/ \
2 3
\
5
不是,因为出现了"有右孩子但没有左孩子"。
要求分别思考:
- 方法一:层序遍历
- 方法二:递归信息模型
思考问题
- 层序遍历中,一旦遇到什么情况,后面的所有节点都必须是叶子?
- 为什么"有右孩子但没有左孩子"可以直接判错?
- 使用
ArrayDeque时,能不能把null直接加入队列? - 递归判断时,为什么除了
isCBT还需要isFull和height? - 左右高度相等时,什么组合可能构成 CBT?
- 左边比右边高 1 时,什么组合可能构成 CBT?
提示 1:层序遍历维护 leafMode
一旦某个节点缺少任意一个孩子,之后层序遍历到的所有节点都必须没有孩子。
提示 2:递归时分类讨论高度
完全二叉树只可能出现"左右等高"或"左边恰好高 1"两大高度关系,再结合满二叉树状态判断。
写完后再看:[查看练习 5 参考答案](#查看练习 5 参考答案)
练习 6:最近公共祖先
给定二叉树中的两个节点 first 和 second,返回它们的最近公共祖先(Lowest Common Ancestor, LCA)。
例如:
text
1
/ \
2 3
/ \
4 5
节点 4 和 5 的最近公共祖先是 2。
节点 2 和 5 的最近公共祖先是 2 本身。
思考问题
- 当前节点需要知道左子树是否出现
first/second吗? - 如果左子树已经找到了最终答案,当前节点还需要重新计算答案吗?
- 判断"当前节点就是 first 或 second"时,应该比较节点引用还是只比较节点值?
- 如果两个目标节点中的某一个根本不在树中,应该返回什么?
提示:Info 可以描述三件事
当前子树是否包含 first、是否包含 second、当前子树中是否已经确定最近公共祖先。
写完后再看:[查看练习 6 参考答案](#查看练习 6 参考答案)
练习 7:二叉树中的最大距离
给定一棵二叉树,返回任意两个节点之间路径的最大长度。
为了避免歧义,本文统一定义:
距离按路径上经过的节点数计算。
因此单个节点的最大距离是:
text
1
例如:
text
1
/ \
2 3
/
4
最长路径可以是:
text
4 -> 2 -> 1 -> 3
经过 4 个节点,所以答案是 4。
思考问题
- 最大距离有没有可能完全位于左子树?
- 有没有可能完全位于右子树?
- 有没有可能必须经过当前节点?
- 如果经过当前节点,路径长度怎样由左右高度得到?
- 为什么递归信息里同时需要
height与maxDistance?
提示:答案只有三种来源
左子树内部、右子树内部、从左边最深处经过当前节点走到右边最深处。
写完后再看:[查看练习 7 参考答案](#查看练习 7 参考答案)
练习 8:派对的最大快乐值
公司的组织结构是一棵没有环的多叉树:
- 老板是根节点;
- 每个员工有唯一直接上级;
- 每个员工有一个快乐值;
- 如果某员工参加派对,那么他的所有直接下级都不能参加;
- 目标是让总快乐值最大。
思考问题
如果当前员工是 x:
- 当
x参加时,他的每个直接下级是什么状态? - 当
x不参加时,每个直接下级应该固定来还是可以自由选择? - 为什么每个节点只需要返回两个值:
yes和no? - 这道题和前面的二叉树递归套路,本质上有什么共同点?
- 为什么这其实是经典的树形动态规划?
提示 1:当前员工来
当前员工来的情况下,每个直接下级都必须不来,所以只能累加下级的 no。
提示 2:当前员工不来
当前员工不来时,每个下级可以在"来"和"不来"中选择快乐值更大的方案。
写完后再看:[查看练习 8 参考答案](#查看练习 8 参考答案)
第二部分:参考答案与图解
到这里再对照答案。
比代码更重要的是检查:你为每道题设计的 Info,是否恰好包含父节点真正需要的信息?
答案 1:判断平衡二叉树
[返回练习 1](#返回练习 1)
问题回顾
当前节点要判断自己平衡,需要:
text
左子树:是否平衡 + 高度
右子树:是否平衡 + 高度
于是当前节点可以得到:
text
isBalanced
=
left.isBalanced
&& right.isBalanced
&& abs(left.height - right.height) <= 1
同时:
text
height = max(left.height, right.height) + 1
信息流
#mermaid-svg-u1PNUbupYwHCIbn6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-u1PNUbupYwHCIbn6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-u1PNUbupYwHCIbn6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-u1PNUbupYwHCIbn6 .error-icon{fill:#552222;}#mermaid-svg-u1PNUbupYwHCIbn6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-u1PNUbupYwHCIbn6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-u1PNUbupYwHCIbn6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-u1PNUbupYwHCIbn6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-u1PNUbupYwHCIbn6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-u1PNUbupYwHCIbn6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-u1PNUbupYwHCIbn6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-u1PNUbupYwHCIbn6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-u1PNUbupYwHCIbn6 .marker.cross{stroke:#333333;}#mermaid-svg-u1PNUbupYwHCIbn6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-u1PNUbupYwHCIbn6 p{margin:0;}#mermaid-svg-u1PNUbupYwHCIbn6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-u1PNUbupYwHCIbn6 .cluster-label text{fill:#333;}#mermaid-svg-u1PNUbupYwHCIbn6 .cluster-label span{color:#333;}#mermaid-svg-u1PNUbupYwHCIbn6 .cluster-label span p{background-color:transparent;}#mermaid-svg-u1PNUbupYwHCIbn6 .label text,#mermaid-svg-u1PNUbupYwHCIbn6 span{fill:#333;color:#333;}#mermaid-svg-u1PNUbupYwHCIbn6 .node rect,#mermaid-svg-u1PNUbupYwHCIbn6 .node circle,#mermaid-svg-u1PNUbupYwHCIbn6 .node ellipse,#mermaid-svg-u1PNUbupYwHCIbn6 .node polygon,#mermaid-svg-u1PNUbupYwHCIbn6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-u1PNUbupYwHCIbn6 .rough-node .label text,#mermaid-svg-u1PNUbupYwHCIbn6 .node .label text,#mermaid-svg-u1PNUbupYwHCIbn6 .image-shape .label,#mermaid-svg-u1PNUbupYwHCIbn6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-u1PNUbupYwHCIbn6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-u1PNUbupYwHCIbn6 .rough-node .label,#mermaid-svg-u1PNUbupYwHCIbn6 .node .label,#mermaid-svg-u1PNUbupYwHCIbn6 .image-shape .label,#mermaid-svg-u1PNUbupYwHCIbn6 .icon-shape .label{text-align:center;}#mermaid-svg-u1PNUbupYwHCIbn6 .node.clickable{cursor:pointer;}#mermaid-svg-u1PNUbupYwHCIbn6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-u1PNUbupYwHCIbn6 .arrowheadPath{fill:#333333;}#mermaid-svg-u1PNUbupYwHCIbn6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-u1PNUbupYwHCIbn6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-u1PNUbupYwHCIbn6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-u1PNUbupYwHCIbn6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-u1PNUbupYwHCIbn6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-u1PNUbupYwHCIbn6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-u1PNUbupYwHCIbn6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-u1PNUbupYwHCIbn6 .cluster text{fill:#333;}#mermaid-svg-u1PNUbupYwHCIbn6 .cluster span{color:#333;}#mermaid-svg-u1PNUbupYwHCIbn6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-u1PNUbupYwHCIbn6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-u1PNUbupYwHCIbn6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-u1PNUbupYwHCIbn6 .icon-shape,#mermaid-svg-u1PNUbupYwHCIbn6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-u1PNUbupYwHCIbn6 .icon-shape p,#mermaid-svg-u1PNUbupYwHCIbn6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-u1PNUbupYwHCIbn6 .icon-shape .label rect,#mermaid-svg-u1PNUbupYwHCIbn6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-u1PNUbupYwHCIbn6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-u1PNUbupYwHCIbn6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-u1PNUbupYwHCIbn6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 左子树 Info
isBalanced + height
当前节点
右子树 Info
isBalanced + height
计算当前 height
计算当前 isBalanced
向父节点返回 Info
完整代码
java
package class07;
import utils.TreeNode;
/**
* 判断二叉树是否为平衡二叉树。
*
* 本题沿用约定:
* 对外接口中 root == null 时返回 false。
*/
public class Code01_IsBalanced {
/**
* 每棵子树向父节点汇报两项信息:
* 1. 自己是否平衡;
* 2. 自己的高度。
*/
private static class Info {
final boolean balanced;
final int height;
Info(boolean balanced, int height) {
this.balanced = balanced;
this.height = height;
}
}
public static boolean isBalanced(TreeNode root) {
if (root == null) {
return false;
}
return process(root).balanced;
}
/**
* 返回"以 root 为根的子树"的完整信息。
*/
private static Info process(TreeNode root) {
// 空树在递归内部视为平衡,高度为 0,
// 这样父节点可以直接统一处理,不必反复判空。
if (root == null) {
return new Info(true, 0);
}
Info left = process(root.left);
Info right = process(root.right);
int height = Math.max(left.height, right.height) + 1;
boolean balanced =
left.balanced
&& right.balanced
&& Math.abs(left.height - right.height) <= 1;
return new Info(balanced, height);
}
}
为什么空树在递归内部返回 true?
因为空树不会破坏父节点的平衡性。
如果一个叶子节点:
text
left -> (true, 0)
right -> (true, 0)
那么:
text
height = 1
高度差 = 0
自然得到叶子节点平衡。
复杂度
| 指标 | 复杂度 |
|---|---|
| 时间复杂度 | O(N) |
| 递归栈 | O(H) |
| 最坏递归栈 | O(N) |
其中 H 为树高。
易错点
- 只返回高度,不返回子树自身是否平衡;
- 只检查当前高度差,却漏掉某个子树内部已经不平衡;
- 混淆"对外空树约定"和"递归内部空树基线"。
答案 2:判断满二叉树
[返回练习 2](#返回练习 2)
问题回顾
本文把满二叉树定义为 Perfect Binary Tree。
当前节点要成为满二叉树,必须同时满足:
text
左子树是满二叉树
右子树是满二叉树
左高度 == 右高度
递归图
#mermaid-svg-JZ2IXrmZt48e4gfj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JZ2IXrmZt48e4gfj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JZ2IXrmZt48e4gfj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JZ2IXrmZt48e4gfj .error-icon{fill:#552222;}#mermaid-svg-JZ2IXrmZt48e4gfj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JZ2IXrmZt48e4gfj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JZ2IXrmZt48e4gfj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JZ2IXrmZt48e4gfj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JZ2IXrmZt48e4gfj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JZ2IXrmZt48e4gfj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JZ2IXrmZt48e4gfj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JZ2IXrmZt48e4gfj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JZ2IXrmZt48e4gfj .marker.cross{stroke:#333333;}#mermaid-svg-JZ2IXrmZt48e4gfj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JZ2IXrmZt48e4gfj p{margin:0;}#mermaid-svg-JZ2IXrmZt48e4gfj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JZ2IXrmZt48e4gfj .cluster-label text{fill:#333;}#mermaid-svg-JZ2IXrmZt48e4gfj .cluster-label span{color:#333;}#mermaid-svg-JZ2IXrmZt48e4gfj .cluster-label span p{background-color:transparent;}#mermaid-svg-JZ2IXrmZt48e4gfj .label text,#mermaid-svg-JZ2IXrmZt48e4gfj span{fill:#333;color:#333;}#mermaid-svg-JZ2IXrmZt48e4gfj .node rect,#mermaid-svg-JZ2IXrmZt48e4gfj .node circle,#mermaid-svg-JZ2IXrmZt48e4gfj .node ellipse,#mermaid-svg-JZ2IXrmZt48e4gfj .node polygon,#mermaid-svg-JZ2IXrmZt48e4gfj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JZ2IXrmZt48e4gfj .rough-node .label text,#mermaid-svg-JZ2IXrmZt48e4gfj .node .label text,#mermaid-svg-JZ2IXrmZt48e4gfj .image-shape .label,#mermaid-svg-JZ2IXrmZt48e4gfj .icon-shape .label{text-anchor:middle;}#mermaid-svg-JZ2IXrmZt48e4gfj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JZ2IXrmZt48e4gfj .rough-node .label,#mermaid-svg-JZ2IXrmZt48e4gfj .node .label,#mermaid-svg-JZ2IXrmZt48e4gfj .image-shape .label,#mermaid-svg-JZ2IXrmZt48e4gfj .icon-shape .label{text-align:center;}#mermaid-svg-JZ2IXrmZt48e4gfj .node.clickable{cursor:pointer;}#mermaid-svg-JZ2IXrmZt48e4gfj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JZ2IXrmZt48e4gfj .arrowheadPath{fill:#333333;}#mermaid-svg-JZ2IXrmZt48e4gfj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JZ2IXrmZt48e4gfj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JZ2IXrmZt48e4gfj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JZ2IXrmZt48e4gfj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JZ2IXrmZt48e4gfj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JZ2IXrmZt48e4gfj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JZ2IXrmZt48e4gfj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JZ2IXrmZt48e4gfj .cluster text{fill:#333;}#mermaid-svg-JZ2IXrmZt48e4gfj .cluster span{color:#333;}#mermaid-svg-JZ2IXrmZt48e4gfj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JZ2IXrmZt48e4gfj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JZ2IXrmZt48e4gfj rect.text{fill:none;stroke-width:0;}#mermaid-svg-JZ2IXrmZt48e4gfj .icon-shape,#mermaid-svg-JZ2IXrmZt48e4gfj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JZ2IXrmZt48e4gfj .icon-shape p,#mermaid-svg-JZ2IXrmZt48e4gfj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JZ2IXrmZt48e4gfj .icon-shape .label rect,#mermaid-svg-JZ2IXrmZt48e4gfj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JZ2IXrmZt48e4gfj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JZ2IXrmZt48e4gfj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JZ2IXrmZt48e4gfj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
是
否
获取左 Info
左右都是 full?
获取右 Info
当前不是满二叉树
左右高度相同?
当前是满二叉树
完整代码
java
package class07;
import utils.TreeNode;
/**
* 判断是否为满二叉树(Perfect Binary Tree)。
*
* 本题沿用约定:
* 对外接口中 root == null 时返回 false。
*/
public class Code02_IsFull {
private static class Info {
final boolean full;
final int height;
Info(boolean full, int height) {
this.full = full;
this.height = height;
}
}
public static boolean isFull(TreeNode root) {
if (root == null) {
return false;
}
return process(root).full;
}
private static Info process(TreeNode root) {
if (root == null) {
return new Info(true, 0);
}
Info left = process(root.left);
Info right = process(root.right);
int height = Math.max(left.height, right.height) + 1;
boolean full =
left.full
&& right.full
&& left.height == right.height;
return new Info(full, height);
}
}
另一种常见判法
如果一棵满二叉树高度为 h,节点数满足:
text
nodes = 2^h - 1
因此也可以递归统计:
text
height
nodes
最终检查:
text
nodes == 2^height - 1
但直接维护 isFull + height 更贴合今天的"递归信息模型"。
复杂度
| 指标 | 复杂度 |
|---|---|
| 时间复杂度 | O(N) |
| 递归栈 | O(H) |
易错点
- 把"Full Binary Tree(每个节点 0 或 2 个孩子)"和"Perfect Binary Tree(每层填满)"混在一起;
- 只看左右是否都有孩子,却不比较子树高度;
- 只比较高度,不检查左右子树自身是否已经违反条件。
答案 3:判断二叉搜索树
[返回练习 3](#返回练习 3)
问题回顾
当前节点成为 BST,需要:
text
左子树本身是 BST
右子树本身是 BST
左子树最大值 < root.value
root.value < 右子树最小值
因此每棵子树至少要返回:
text
isBST
min
max
为什么不能只比较直接孩子?
下面这棵树:
text
10
/ \
5 15
/
6
如果只比较:
text
5 < 10 < 15
似乎没有问题。
但 6 位于 10 的右子树,却小于 10,所以整棵树不是 BST。
信息合并
#mermaid-svg-z3gxeio7SctnB3PR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-z3gxeio7SctnB3PR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-z3gxeio7SctnB3PR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-z3gxeio7SctnB3PR .error-icon{fill:#552222;}#mermaid-svg-z3gxeio7SctnB3PR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-z3gxeio7SctnB3PR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-z3gxeio7SctnB3PR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-z3gxeio7SctnB3PR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-z3gxeio7SctnB3PR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-z3gxeio7SctnB3PR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-z3gxeio7SctnB3PR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-z3gxeio7SctnB3PR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-z3gxeio7SctnB3PR .marker.cross{stroke:#333333;}#mermaid-svg-z3gxeio7SctnB3PR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-z3gxeio7SctnB3PR p{margin:0;}#mermaid-svg-z3gxeio7SctnB3PR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-z3gxeio7SctnB3PR .cluster-label text{fill:#333;}#mermaid-svg-z3gxeio7SctnB3PR .cluster-label span{color:#333;}#mermaid-svg-z3gxeio7SctnB3PR .cluster-label span p{background-color:transparent;}#mermaid-svg-z3gxeio7SctnB3PR .label text,#mermaid-svg-z3gxeio7SctnB3PR span{fill:#333;color:#333;}#mermaid-svg-z3gxeio7SctnB3PR .node rect,#mermaid-svg-z3gxeio7SctnB3PR .node circle,#mermaid-svg-z3gxeio7SctnB3PR .node ellipse,#mermaid-svg-z3gxeio7SctnB3PR .node polygon,#mermaid-svg-z3gxeio7SctnB3PR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-z3gxeio7SctnB3PR .rough-node .label text,#mermaid-svg-z3gxeio7SctnB3PR .node .label text,#mermaid-svg-z3gxeio7SctnB3PR .image-shape .label,#mermaid-svg-z3gxeio7SctnB3PR .icon-shape .label{text-anchor:middle;}#mermaid-svg-z3gxeio7SctnB3PR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-z3gxeio7SctnB3PR .rough-node .label,#mermaid-svg-z3gxeio7SctnB3PR .node .label,#mermaid-svg-z3gxeio7SctnB3PR .image-shape .label,#mermaid-svg-z3gxeio7SctnB3PR .icon-shape .label{text-align:center;}#mermaid-svg-z3gxeio7SctnB3PR .node.clickable{cursor:pointer;}#mermaid-svg-z3gxeio7SctnB3PR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-z3gxeio7SctnB3PR .arrowheadPath{fill:#333333;}#mermaid-svg-z3gxeio7SctnB3PR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-z3gxeio7SctnB3PR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-z3gxeio7SctnB3PR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-z3gxeio7SctnB3PR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-z3gxeio7SctnB3PR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-z3gxeio7SctnB3PR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-z3gxeio7SctnB3PR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-z3gxeio7SctnB3PR .cluster text{fill:#333;}#mermaid-svg-z3gxeio7SctnB3PR .cluster span{color:#333;}#mermaid-svg-z3gxeio7SctnB3PR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-z3gxeio7SctnB3PR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-z3gxeio7SctnB3PR rect.text{fill:none;stroke-width:0;}#mermaid-svg-z3gxeio7SctnB3PR .icon-shape,#mermaid-svg-z3gxeio7SctnB3PR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-z3gxeio7SctnB3PR .icon-shape p,#mermaid-svg-z3gxeio7SctnB3PR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-z3gxeio7SctnB3PR .icon-shape .label rect,#mermaid-svg-z3gxeio7SctnB3PR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-z3gxeio7SctnB3PR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-z3gxeio7SctnB3PR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-z3gxeio7SctnB3PR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 左子树
isBST min max
当前节点
右子树
isBST min max
计算当前 min / max
检查 left.max < value
检查 value < right.min
返回当前 Info
完整代码
java
package class07;
import utils.TreeNode;
/**
* 判断是否为严格二叉搜索树。
*
* 严格定义:
* 左子树所有值 < 当前值 < 右子树所有值。
*/
public class Code03_IsBST {
private static class Info {
final boolean bst;
final int min;
final int max;
Info(boolean bst, int min, int max) {
this.bst = bst;
this.min = min;
this.max = max;
}
}
public static boolean isBST(TreeNode root) {
if (root == null) {
return false;
}
return process(root).bst;
}
private static Info process(TreeNode root) {
if (root == null) {
return null;
}
Info left = process(root.left);
Info right = process(root.right);
int min = root.value;
int max = root.value;
if (left != null) {
min = Math.min(min, left.min);
max = Math.max(max, left.max);
}
if (right != null) {
min = Math.min(min, right.min);
max = Math.max(max, right.max);
}
boolean leftValid =
left == null
|| (left.bst && left.max < root.value);
boolean rightValid =
right == null
|| (right.bst && root.value < right.min);
return new Info(leftValid && rightValid, min, max);
}
}
这里的关键不是 min / max,而是"全局约束"
BST 的约束不是:
text
父亲只管直接孩子
而是:
text
父亲必须压住整棵左子树的最大值
父亲必须小于整棵右子树的最小值
这也是为什么递归信息要向上汇总"极值"。
复杂度
| 指标 | 复杂度 |
|---|---|
| 时间复杂度 | O(N) |
| 递归栈 | O(H) |
易错点
- 只比较左右孩子;
- 忘记检查左右子树自身是否为 BST;
- 严格 BST 与允许重复值的 BST 规则混用;
min / max更新漏掉当前节点自身。
答案 4:最大搜索二叉子树的大小
[返回练习 4](#返回练习 4)
问题回顾
这道题比"判断 BST"多了一层:
不仅要知道当前子树是不是 BST,还要知道当前范围内最大的 BST 有多大。
所以每棵子树可以返回:
text
isBST
min
max
size
maxSubBSTSize
其中:
size:当前子树总节点数;maxSubBSTSize:当前子树范围内最大的 BST 节点数。
三种答案来源
#mermaid-svg-UeDAGvAEvJe9t89f{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UeDAGvAEvJe9t89f .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UeDAGvAEvJe9t89f .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UeDAGvAEvJe9t89f .error-icon{fill:#552222;}#mermaid-svg-UeDAGvAEvJe9t89f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UeDAGvAEvJe9t89f .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UeDAGvAEvJe9t89f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UeDAGvAEvJe9t89f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UeDAGvAEvJe9t89f .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UeDAGvAEvJe9t89f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UeDAGvAEvJe9t89f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UeDAGvAEvJe9t89f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UeDAGvAEvJe9t89f .marker.cross{stroke:#333333;}#mermaid-svg-UeDAGvAEvJe9t89f svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UeDAGvAEvJe9t89f p{margin:0;}#mermaid-svg-UeDAGvAEvJe9t89f .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UeDAGvAEvJe9t89f .cluster-label text{fill:#333;}#mermaid-svg-UeDAGvAEvJe9t89f .cluster-label span{color:#333;}#mermaid-svg-UeDAGvAEvJe9t89f .cluster-label span p{background-color:transparent;}#mermaid-svg-UeDAGvAEvJe9t89f .label text,#mermaid-svg-UeDAGvAEvJe9t89f span{fill:#333;color:#333;}#mermaid-svg-UeDAGvAEvJe9t89f .node rect,#mermaid-svg-UeDAGvAEvJe9t89f .node circle,#mermaid-svg-UeDAGvAEvJe9t89f .node ellipse,#mermaid-svg-UeDAGvAEvJe9t89f .node polygon,#mermaid-svg-UeDAGvAEvJe9t89f .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UeDAGvAEvJe9t89f .rough-node .label text,#mermaid-svg-UeDAGvAEvJe9t89f .node .label text,#mermaid-svg-UeDAGvAEvJe9t89f .image-shape .label,#mermaid-svg-UeDAGvAEvJe9t89f .icon-shape .label{text-anchor:middle;}#mermaid-svg-UeDAGvAEvJe9t89f .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UeDAGvAEvJe9t89f .rough-node .label,#mermaid-svg-UeDAGvAEvJe9t89f .node .label,#mermaid-svg-UeDAGvAEvJe9t89f .image-shape .label,#mermaid-svg-UeDAGvAEvJe9t89f .icon-shape .label{text-align:center;}#mermaid-svg-UeDAGvAEvJe9t89f .node.clickable{cursor:pointer;}#mermaid-svg-UeDAGvAEvJe9t89f .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UeDAGvAEvJe9t89f .arrowheadPath{fill:#333333;}#mermaid-svg-UeDAGvAEvJe9t89f .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UeDAGvAEvJe9t89f .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UeDAGvAEvJe9t89f .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UeDAGvAEvJe9t89f .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UeDAGvAEvJe9t89f .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UeDAGvAEvJe9t89f .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UeDAGvAEvJe9t89f .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UeDAGvAEvJe9t89f .cluster text{fill:#333;}#mermaid-svg-UeDAGvAEvJe9t89f .cluster span{color:#333;}#mermaid-svg-UeDAGvAEvJe9t89f div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UeDAGvAEvJe9t89f .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UeDAGvAEvJe9t89f rect.text{fill:none;stroke-width:0;}#mermaid-svg-UeDAGvAEvJe9t89f .icon-shape,#mermaid-svg-UeDAGvAEvJe9t89f .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UeDAGvAEvJe9t89f .icon-shape p,#mermaid-svg-UeDAGvAEvJe9t89f .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UeDAGvAEvJe9t89f .icon-shape .label rect,#mermaid-svg-UeDAGvAEvJe9t89f .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UeDAGvAEvJe9t89f .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UeDAGvAEvJe9t89f .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UeDAGvAEvJe9t89f :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 当前子树的最大 BST
完全在左子树
完全在右子树
就是当前整棵子树
完整代码
java
package class07;
import utils.TreeNode;
/**
* 返回二叉树中最大二叉搜索子树的节点数。
*/
public class Code04_MaxSubBSTSize {
private static class Info {
final boolean bst;
final int min;
final int max;
// 以当前节点为根的整棵子树节点数。
final int size;
// 当前子树范围内,最大 BST 的节点数。
final int maxSubBSTSize;
Info(boolean bst, int min, int max, int size, int maxSubBSTSize) {
this.bst = bst;
this.min = min;
this.max = max;
this.size = size;
this.maxSubBSTSize = maxSubBSTSize;
}
}
public static int maxSubBSTSize(TreeNode root) {
if (root == null) {
return 0;
}
return process(root).maxSubBSTSize;
}
private static Info process(TreeNode root) {
if (root == null) {
return null;
}
Info left = process(root.left);
Info right = process(root.right);
int size = 1;
int min = root.value;
int max = root.value;
if (left != null) {
size += left.size;
min = Math.min(min, left.min);
max = Math.max(max, left.max);
}
if (right != null) {
size += right.size;
min = Math.min(min, right.min);
max = Math.max(max, right.max);
}
boolean leftValid =
left == null
|| (left.bst && left.max < root.value);
boolean rightValid =
right == null
|| (right.bst && root.value < right.min);
boolean bst = leftValid && rightValid;
int leftBest = left == null ? 0 : left.maxSubBSTSize;
int rightBest = right == null ? 0 : right.maxSubBSTSize;
int maxSubBSTSize;
if (bst) {
// 当前整棵子树就是 BST,
// 最大 BST 的大小就是当前子树的总节点数。
maxSubBSTSize = size;
} else {
// 当前整棵不是 BST,
// 最优答案只能来自左右子树。
maxSubBSTSize = Math.max(leftBest, rightBest);
}
return new Info(bst, min, max, size, maxSubBSTSize);
}
}
为什么需要 size?
假设:
text
左子树总大小 = 7
左子树最大 BST = 3
右子树总大小 = 5
右子树最大 BST = 2
如果当前整棵最终被证明是 BST,那么左右子树实际上也必须整体是 BST。
因此真正需要的是:
text
7 + 5 + 1 = 13
而不能写:
text
max(3, 2) + 1 = 4
复杂度
| 指标 | 复杂度 |
|---|---|
| 时间复杂度 | O(N) |
| 递归栈 | O(H) |
易错点
- 把"子树总大小"和"子树内部最大 BST 大小"混为一谈;
- 当前整棵是 BST 时只执行
best + 1; - 判断 BST 时忘记左右子树本身也必须是 BST;
- 极值信息没有继续向父节点传递。
答案 5:判断完全二叉树
[返回练习 5](#返回练习 5)
方法一:层序遍历
核心规则
层序遍历时有两个直接判错规则:
text
规则 1:
有右孩子,但没有左孩子 -> false
以及:
text
规则 2:
一旦遇到"孩子不双全"的节点,
后面所有节点都必须是叶子节点
状态图
#mermaid-svg-Wj4LlSBDWJ1RNlw1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .error-icon{fill:#552222;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .marker.cross{stroke:#333333;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 p{margin:0;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .cluster-label text{fill:#333;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .cluster-label span{color:#333;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .cluster-label span p{background-color:transparent;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .label text,#mermaid-svg-Wj4LlSBDWJ1RNlw1 span{fill:#333;color:#333;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .node rect,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .node circle,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .node ellipse,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .node polygon,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .rough-node .label text,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .node .label text,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .image-shape .label,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .rough-node .label,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .node .label,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .image-shape .label,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .icon-shape .label{text-align:center;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .node.clickable{cursor:pointer;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .arrowheadPath{fill:#333333;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .cluster text{fill:#333;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .cluster span{color:#333;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .icon-shape,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .icon-shape p,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .icon-shape .label rect,#mermaid-svg-Wj4LlSBDWJ1RNlw1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Wj4LlSBDWJ1RNlw1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
层序取出当前节点
无左但有右?
返回 false
已进入 leafMode 且当前还有孩子?
非空孩子入队
当前缺少任意孩子?
leafMode = true
继续
完整代码
java
package class07;
import utils.TreeNode;
import java.util.ArrayDeque;
import java.util.Queue;
/**
* 判断完全二叉树。
*/
public class Code05_IsCBT {
/**
* 本题沿用约定:
* root == null 时对外返回 false。
*/
public static boolean isCBT(TreeNode root) {
if (root == null) {
return false;
}
return bfs(root);
}
private static boolean bfs(TreeNode root) {
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
// 一旦进入 leafMode,
// 后面遍历到的节点都必须是叶子。
boolean leafMode = false;
while (!queue.isEmpty()) {
TreeNode cur = queue.poll();
TreeNode left = cur.left;
TreeNode right = cur.right;
// 有右无左,直接违反完全二叉树定义。
if (left == null && right != null) {
return false;
}
// 已经进入"后续必须是叶子"的阶段,
// 当前却仍然有孩子。
if (leafMode && (left != null || right != null)) {
return false;
}
if (left != null) {
queue.offer(left);
}
if (right != null) {
queue.offer(right);
}
// 只要孩子不双全,从下一个节点开始必须全是叶子。
if (left == null || right == null) {
leafMode = true;
}
}
return true;
}
}
方法二:递归信息模型
每棵子树向上返回:
text
isFull
isCBT
height
完全二叉树的递归组合
可以压缩成:
text
情况 A:
左满,右 CBT,左右等高
情况 B:
左 CBT,右满,左比右高 1
因为满二叉树本身也一定是完全二叉树,所以这两条可以覆盖标准组合。
递归版代码
java
private static class Info {
final boolean full;
final boolean cbt;
final int height;
Info(boolean full, boolean cbt, int height) {
this.full = full;
this.cbt = cbt;
this.height = height;
}
}
private static Info process(TreeNode root) {
if (root == null) {
return new Info(true, true, 0);
}
Info left = process(root.left);
Info right = process(root.right);
int height = Math.max(left.height, right.height) + 1;
boolean full =
left.full
&& right.full
&& left.height == right.height;
boolean cbt =
// 左右等高:
// 左边必须满,右边可以是完全二叉树。
(left.full
&& right.cbt
&& left.height == right.height)
||
// 左边恰好高 1:
// 左边可以是完全二叉树,右边必须满。
(left.cbt
&& right.full
&& left.height == right.height + 1);
return new Info(full, cbt, height);
}
如果要对外使用:
java
public static boolean isCBTByRecursion(TreeNode root) {
return root != null && process(root).cbt;
}
两种方法怎么选?
| 方法 | 优点 | 重点 |
|---|---|---|
| 层序遍历 | 直观、规则少 | leafMode |
| 递归信息 | 能训练树形 DP | full + cbt + height |
复杂度
两种方法都是:
| 指标 | 复杂度 |
|---|---|
| 时间复杂度 | O(N) |
| BFS 队列空间 | O(W) |
| 递归栈空间 | O(H) |
W 是树的最大宽度。
易错点
- 往
ArrayDeque中加入null; - 只检查"有右无左",漏掉进入
leafMode后还有孩子; - 递归法只返回
isCBT,导致父节点无法判断组合情况; - 把满二叉树与完全二叉树概念混淆。
答案 6:最近公共祖先
[返回练习 6](#返回练习 6)
问题回顾
每棵子树返回:
text
foundFirst
foundSecond
ancestor
含义分别是:
- 当前子树里是否找到
first; - 当前子树里是否找到
second; - 当前子树里是否已经确定最终最近公共祖先。
递归过程
#mermaid-svg-rzX01PkbysltPhMj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rzX01PkbysltPhMj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rzX01PkbysltPhMj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rzX01PkbysltPhMj .error-icon{fill:#552222;}#mermaid-svg-rzX01PkbysltPhMj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rzX01PkbysltPhMj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rzX01PkbysltPhMj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rzX01PkbysltPhMj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rzX01PkbysltPhMj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rzX01PkbysltPhMj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rzX01PkbysltPhMj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rzX01PkbysltPhMj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rzX01PkbysltPhMj .marker.cross{stroke:#333333;}#mermaid-svg-rzX01PkbysltPhMj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rzX01PkbysltPhMj p{margin:0;}#mermaid-svg-rzX01PkbysltPhMj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rzX01PkbysltPhMj .cluster-label text{fill:#333;}#mermaid-svg-rzX01PkbysltPhMj .cluster-label span{color:#333;}#mermaid-svg-rzX01PkbysltPhMj .cluster-label span p{background-color:transparent;}#mermaid-svg-rzX01PkbysltPhMj .label text,#mermaid-svg-rzX01PkbysltPhMj span{fill:#333;color:#333;}#mermaid-svg-rzX01PkbysltPhMj .node rect,#mermaid-svg-rzX01PkbysltPhMj .node circle,#mermaid-svg-rzX01PkbysltPhMj .node ellipse,#mermaid-svg-rzX01PkbysltPhMj .node polygon,#mermaid-svg-rzX01PkbysltPhMj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rzX01PkbysltPhMj .rough-node .label text,#mermaid-svg-rzX01PkbysltPhMj .node .label text,#mermaid-svg-rzX01PkbysltPhMj .image-shape .label,#mermaid-svg-rzX01PkbysltPhMj .icon-shape .label{text-anchor:middle;}#mermaid-svg-rzX01PkbysltPhMj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rzX01PkbysltPhMj .rough-node .label,#mermaid-svg-rzX01PkbysltPhMj .node .label,#mermaid-svg-rzX01PkbysltPhMj .image-shape .label,#mermaid-svg-rzX01PkbysltPhMj .icon-shape .label{text-align:center;}#mermaid-svg-rzX01PkbysltPhMj .node.clickable{cursor:pointer;}#mermaid-svg-rzX01PkbysltPhMj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rzX01PkbysltPhMj .arrowheadPath{fill:#333333;}#mermaid-svg-rzX01PkbysltPhMj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rzX01PkbysltPhMj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rzX01PkbysltPhMj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rzX01PkbysltPhMj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rzX01PkbysltPhMj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rzX01PkbysltPhMj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rzX01PkbysltPhMj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rzX01PkbysltPhMj .cluster text{fill:#333;}#mermaid-svg-rzX01PkbysltPhMj .cluster span{color:#333;}#mermaid-svg-rzX01PkbysltPhMj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rzX01PkbysltPhMj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rzX01PkbysltPhMj rect.text{fill:none;stroke-width:0;}#mermaid-svg-rzX01PkbysltPhMj .icon-shape,#mermaid-svg-rzX01PkbysltPhMj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rzX01PkbysltPhMj .icon-shape p,#mermaid-svg-rzX01PkbysltPhMj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rzX01PkbysltPhMj .icon-shape .label rect,#mermaid-svg-rzX01PkbysltPhMj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rzX01PkbysltPhMj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rzX01PkbysltPhMj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rzX01PkbysltPhMj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
左子树 Info
当前节点
右子树 Info
子树中已有 ancestor?
答案直接向上返回
汇总 first / second 是否出现
两者都出现?
当前节点成为 ancestor
ancestor = null
完整代码
java
package class07;
import utils.TreeNode;
/**
* 找到二叉树中两个指定节点的最近公共祖先。
*/
public class Code06_LowestAncestor {
private static class Info {
final boolean foundFirst;
final boolean foundSecond;
final TreeNode ancestor;
Info(boolean foundFirst, boolean foundSecond, TreeNode ancestor) {
this.foundFirst = foundFirst;
this.foundSecond = foundSecond;
this.ancestor = ancestor;
}
}
public static TreeNode lowestCommonAncestor(
TreeNode root,
TreeNode first,
TreeNode second) {
if (root == null || first == null || second == null) {
return null;
}
return process(root, first, second).ancestor;
}
private static Info process(
TreeNode root,
TreeNode first,
TreeNode second) {
if (root == null) {
return new Info(false, false, null);
}
Info left = process(root.left, first, second);
Info right = process(root.right, first, second);
boolean foundFirst =
left.foundFirst
|| right.foundFirst
|| root == first;
boolean foundSecond =
left.foundSecond
|| right.foundSecond
|| root == second;
TreeNode ancestor = null;
// 如果某一侧已经确定了最近公共祖先,
// 当前节点只负责把这个答案继续向上带。
if (left.ancestor != null) {
ancestor = left.ancestor;
} else if (right.ancestor != null) {
ancestor = right.ancestor;
} else if (foundFirst && foundSecond) {
// 左右子树都没有更低的答案,
// 但当前子树第一次同时拥有两个目标节点,
// 所以当前节点就是最近公共祖先。
ancestor = root;
}
return new Info(foundFirst, foundSecond, ancestor);
}
}
手推:2 和 5
text
1
/ \
2 3
/ \
4 5
处理节点 2:
text
root == first -> foundFirst = true
右子树找到 5 -> foundSecond = true
左右都没有更低 ancestor
因此:
text
ancestor = 2
这也说明:
一个节点可以是它自己的祖先。
不存在节点怎么办?
如果某个目标节点不在树中,则根节点最终不会同时满足:
text
foundFirst && foundSecond
因此 ancestor 会保持 null。
复杂度
| 指标 | 复杂度 |
|---|---|
| 时间复杂度 | O(N) |
| 递归栈 | O(H) |
易错点
- 按节点值比较,而不是按节点身份比较;
- 找到一侧答案后又被上层覆盖;
- 忘记"某个目标节点本身就是祖先"的情况;
- 默认假设两个节点一定存在于树中。
答案 7:二叉树中的最大距离
[返回练习 7](#返回练习 7)
问题回顾
最大距离只有三种来源:
text
1. 完全在左子树
2. 完全在右子树
3. 穿过当前节点
因此:
text
maxDistance
=
max(
left.maxDistance,
right.maxDistance,
left.height + right.height + 1
)
这里的 +1 表示当前节点本身。
图解
text
左侧最深节点
*
|
|
L
\
当前节点
/
R
|
|
*
右侧最深节点
穿过当前节点的最长路径:
text
左高度 + 当前节点 1 个 + 右高度
递归信息流
#mermaid-svg-xpH30J9W9is8TI6u{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xpH30J9W9is8TI6u .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xpH30J9W9is8TI6u .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xpH30J9W9is8TI6u .error-icon{fill:#552222;}#mermaid-svg-xpH30J9W9is8TI6u .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xpH30J9W9is8TI6u .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xpH30J9W9is8TI6u .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xpH30J9W9is8TI6u .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xpH30J9W9is8TI6u .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xpH30J9W9is8TI6u .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xpH30J9W9is8TI6u .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xpH30J9W9is8TI6u .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xpH30J9W9is8TI6u .marker.cross{stroke:#333333;}#mermaid-svg-xpH30J9W9is8TI6u svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xpH30J9W9is8TI6u p{margin:0;}#mermaid-svg-xpH30J9W9is8TI6u .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xpH30J9W9is8TI6u .cluster-label text{fill:#333;}#mermaid-svg-xpH30J9W9is8TI6u .cluster-label span{color:#333;}#mermaid-svg-xpH30J9W9is8TI6u .cluster-label span p{background-color:transparent;}#mermaid-svg-xpH30J9W9is8TI6u .label text,#mermaid-svg-xpH30J9W9is8TI6u span{fill:#333;color:#333;}#mermaid-svg-xpH30J9W9is8TI6u .node rect,#mermaid-svg-xpH30J9W9is8TI6u .node circle,#mermaid-svg-xpH30J9W9is8TI6u .node ellipse,#mermaid-svg-xpH30J9W9is8TI6u .node polygon,#mermaid-svg-xpH30J9W9is8TI6u .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xpH30J9W9is8TI6u .rough-node .label text,#mermaid-svg-xpH30J9W9is8TI6u .node .label text,#mermaid-svg-xpH30J9W9is8TI6u .image-shape .label,#mermaid-svg-xpH30J9W9is8TI6u .icon-shape .label{text-anchor:middle;}#mermaid-svg-xpH30J9W9is8TI6u .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xpH30J9W9is8TI6u .rough-node .label,#mermaid-svg-xpH30J9W9is8TI6u .node .label,#mermaid-svg-xpH30J9W9is8TI6u .image-shape .label,#mermaid-svg-xpH30J9W9is8TI6u .icon-shape .label{text-align:center;}#mermaid-svg-xpH30J9W9is8TI6u .node.clickable{cursor:pointer;}#mermaid-svg-xpH30J9W9is8TI6u .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xpH30J9W9is8TI6u .arrowheadPath{fill:#333333;}#mermaid-svg-xpH30J9W9is8TI6u .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xpH30J9W9is8TI6u .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xpH30J9W9is8TI6u .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xpH30J9W9is8TI6u .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xpH30J9W9is8TI6u .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xpH30J9W9is8TI6u .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xpH30J9W9is8TI6u .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xpH30J9W9is8TI6u .cluster text{fill:#333;}#mermaid-svg-xpH30J9W9is8TI6u .cluster span{color:#333;}#mermaid-svg-xpH30J9W9is8TI6u div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xpH30J9W9is8TI6u .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xpH30J9W9is8TI6u rect.text{fill:none;stroke-width:0;}#mermaid-svg-xpH30J9W9is8TI6u .icon-shape,#mermaid-svg-xpH30J9W9is8TI6u .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xpH30J9W9is8TI6u .icon-shape p,#mermaid-svg-xpH30J9W9is8TI6u .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xpH30J9W9is8TI6u .icon-shape .label rect,#mermaid-svg-xpH30J9W9is8TI6u .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xpH30J9W9is8TI6u .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xpH30J9W9is8TI6u .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xpH30J9W9is8TI6u :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 左子树
height + maxDistance
当前节点
右子树
height + maxDistance
height = max左右高度 + 1
throughRoot = 左高 + 右高 + 1
maxDistance 取三者最大
返回 Info
完整代码
java
package class07;
import utils.TreeNode;
/**
* 二叉树中任意两个节点之间的最大距离。
*
* 本文约定:
* 距离按路径上经过的"节点数"计算。
*/
public class Code07_MaxDistance {
private static class Info {
final int maxDistance;
final int height;
Info(int maxDistance, int height) {
this.maxDistance = maxDistance;
this.height = height;
}
}
public static int maxDistance(TreeNode root) {
if (root == null) {
return 0;
}
return process(root).maxDistance;
}
private static Info process(TreeNode root) {
if (root == null) {
return new Info(0, 0);
}
Info left = process(root.left);
Info right = process(root.right);
int height = Math.max(left.height, right.height) + 1;
// 最大路径如果经过当前节点:
// 左侧最深路径 + 当前节点 + 右侧最深路径。
int throughRoot = left.height + right.height + 1;
int maxDistance =
Math.max(
throughRoot,
Math.max(left.maxDistance, right.maxDistance));
return new Info(maxDistance, height);
}
}
原公式为什么容易少 1?
如果写成:
java
left.height + right.height
得到的更接近"边数"定义。
例如单节点:
text
height = 1
如果按节点数,最大距离应该是:
text
1
如果公式没有 +1,结果会变成 0。
所以这道题写代码前必须先统一:
text
距离到底按边数还是按节点数?
如果你想按"边数"定义
可以保留:
java
left.height + right.height
并约定:
text
单节点距离 = 0
两种定义都可以,关键是全篇一致。
复杂度
| 指标 | 复杂度 |
|---|---|
| 时间复杂度 | O(N) |
| 递归栈 | O(H) |
易错点
- 没有先约定距离定义;
- 漏掉"最大路径完全在某一棵子树内部"的情况;
- 穿过当前节点时漏掉当前节点自身;
- 把
height和maxDistance当成同一个概念。
答案 8:派对的最大快乐值
[返回练习 8](#返回练习 8)
问题回顾
对于每个员工 x,只考虑两个状态:
text
yes:x 来时,这棵组织子树能获得的最大快乐值
no :x 不来时,这棵组织子树能获得的最大快乐值
状态转移
如果 x 来:
text
x 的直接下级全部不能来
所以:
text
yes = x.happy + Σ child.no
如果 x 不来:
text
每个下级可以自由选择来或不来
所以:
text
no = Σ max(child.yes, child.no)
图解
#mermaid-svg-WYB3kprIBWOo567p{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WYB3kprIBWOo567p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WYB3kprIBWOo567p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WYB3kprIBWOo567p .error-icon{fill:#552222;}#mermaid-svg-WYB3kprIBWOo567p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WYB3kprIBWOo567p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WYB3kprIBWOo567p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WYB3kprIBWOo567p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WYB3kprIBWOo567p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WYB3kprIBWOo567p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WYB3kprIBWOo567p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WYB3kprIBWOo567p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WYB3kprIBWOo567p .marker.cross{stroke:#333333;}#mermaid-svg-WYB3kprIBWOo567p svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WYB3kprIBWOo567p p{margin:0;}#mermaid-svg-WYB3kprIBWOo567p .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WYB3kprIBWOo567p .cluster-label text{fill:#333;}#mermaid-svg-WYB3kprIBWOo567p .cluster-label span{color:#333;}#mermaid-svg-WYB3kprIBWOo567p .cluster-label span p{background-color:transparent;}#mermaid-svg-WYB3kprIBWOo567p .label text,#mermaid-svg-WYB3kprIBWOo567p span{fill:#333;color:#333;}#mermaid-svg-WYB3kprIBWOo567p .node rect,#mermaid-svg-WYB3kprIBWOo567p .node circle,#mermaid-svg-WYB3kprIBWOo567p .node ellipse,#mermaid-svg-WYB3kprIBWOo567p .node polygon,#mermaid-svg-WYB3kprIBWOo567p .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WYB3kprIBWOo567p .rough-node .label text,#mermaid-svg-WYB3kprIBWOo567p .node .label text,#mermaid-svg-WYB3kprIBWOo567p .image-shape .label,#mermaid-svg-WYB3kprIBWOo567p .icon-shape .label{text-anchor:middle;}#mermaid-svg-WYB3kprIBWOo567p .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WYB3kprIBWOo567p .rough-node .label,#mermaid-svg-WYB3kprIBWOo567p .node .label,#mermaid-svg-WYB3kprIBWOo567p .image-shape .label,#mermaid-svg-WYB3kprIBWOo567p .icon-shape .label{text-align:center;}#mermaid-svg-WYB3kprIBWOo567p .node.clickable{cursor:pointer;}#mermaid-svg-WYB3kprIBWOo567p .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WYB3kprIBWOo567p .arrowheadPath{fill:#333333;}#mermaid-svg-WYB3kprIBWOo567p .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WYB3kprIBWOo567p .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WYB3kprIBWOo567p .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WYB3kprIBWOo567p .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WYB3kprIBWOo567p .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WYB3kprIBWOo567p .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WYB3kprIBWOo567p .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WYB3kprIBWOo567p .cluster text{fill:#333;}#mermaid-svg-WYB3kprIBWOo567p .cluster span{color:#333;}#mermaid-svg-WYB3kprIBWOo567p div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WYB3kprIBWOo567p .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WYB3kprIBWOo567p rect.text{fill:none;stroke-width:0;}#mermaid-svg-WYB3kprIBWOo567p .icon-shape,#mermaid-svg-WYB3kprIBWOo567p .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WYB3kprIBWOo567p .icon-shape p,#mermaid-svg-WYB3kprIBWOo567p .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WYB3kprIBWOo567p .icon-shape .label rect,#mermaid-svg-WYB3kprIBWOo567p .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WYB3kprIBWOo567p .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WYB3kprIBWOo567p .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WYB3kprIBWOo567p :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 参加
不参加
员工 X
X 是否参加?
所有直接下级必须不参加
yes = X.happy + Σ child.no
每个下级自由选择最优状态
no = Σ max(child.yes, child.no)
为什么这是树形 DP?
普通动态规划会写:
text
dp[i][state]
这道题只是把数组下标 i 换成了树节点:
text
dp[employee][come / notCome]
因为组织结构天然是一棵树,所以每个员工的最优决策依赖其孩子状态。
完整代码
java
package class07;
import java.util.ArrayList;
import java.util.List;
/**
* 派对最大快乐值。
*/
public class Code08_MaxHappy {
/**
* 员工节点。
*/
public static class Employee {
final int happy;
final List<Employee> subordinates;
public Employee(int happy) {
this.happy = happy;
this.subordinates = new ArrayList<>();
}
public void addSubordinate(Employee employee) {
if (employee != null) {
subordinates.add(employee);
}
}
}
/**
* yes:当前员工参加时,本子树最大快乐值。
* no :当前员工不参加时,本子树最大快乐值。
*/
private static class Info {
final int yes;
final int no;
Info(int yes, int no) {
this.yes = yes;
this.no = no;
}
}
public static int maxHappy(Employee boss) {
if (boss == null) {
return 0;
}
Info info = process(boss);
return Math.max(info.yes, info.no);
}
private static Info process(Employee employee) {
int yes = employee.happy;
int no = 0;
for (Employee child : employee.subordinates) {
Info next = process(child);
// 当前员工来:
// 直接下级必须不来。
yes += next.no;
// 当前员工不来:
// 下级自己选择"来 / 不来"中更优的一种。
no += Math.max(next.yes, next.no);
}
return new Info(yes, no);
}
}
为什么不需要单独处理叶子节点?
叶子员工:
text
subordinates.isEmpty()
循环不会执行,因此自然得到:
text
yes = employee.happy
no = 0
这已经是正确基线。
所以可以省掉:
java
if (root.subordinates.isEmpty()) {
...
}
让代码更统一。
一个小例子
text
Boss(10)
/ \
A(5) B(20)
/
C(100)
对于 A:
text
A 来:
5 + C.no = 5
A 不来:
max(C.yes, C.no) = 100
因此:
text
A = (yes=5, no=100)
对于 Boss:
text
Boss 来:
10 + A.no + B.no
= 10 + 100 + 0
= 110
text
Boss 不来:
max(A.yes, A.no) + max(B.yes, B.no)
= 100 + 20
= 120
最终:
text
max(110, 120) = 120
复杂度
设员工总数为 N:
| 指标 | 复杂度 |
|---|---|
| 时间复杂度 | O(N) |
| 递归栈 | O(H) |
每个员工只处理一次。
易错点
- 当前员工来时还允许直接下级来;
- 当前员工不来时,强制所有下级都来;
- 每个下级没有独立取
max(yes, no); subordinates可能没有初始化,造成空指针;- 把"直接下级不能来"误解成"所有后代都不能来"。
第三部分:把 8 道题统一成一个递归模板
做到这里,你会发现代码长得越来越像。
它们的共同结构几乎都是:
java
Info process(Node x) {
if (x == null) {
return baseInfo;
}
Info leftInfo = process(x.left);
Info rightInfo = process(x.right);
// 使用左右信息加工出当前信息
...
return new Info(...);
}
真正变化的不是递归骨架,而是:
text
Info 里到底装什么?
1. 平衡二叉树
text
Info:
- isBalanced
- height
父节点需要:
text
孩子是否平衡
孩子有多高
2. 满二叉树
text
Info:
- isFull
- height
父节点需要:
text
孩子是否满
左右高度是否一致
3. BST
text
Info:
- isBST
- min
- max
父节点需要:
text
孩子自身是否合法
左边最大值
右边最小值
4. 最大 BST 子树
text
Info:
- isBST
- min
- max
- size
- maxSubBSTSize
这里开始出现两个层面的信息:
text
"我自己整体是什么状态"
+
"我内部最优答案是多少"
这类题非常典型。
5. 完全二叉树
text
Info:
- isFull
- isCBT
- height
为什么需要 isFull?
因为父节点判断自己是不是 CBT 时,必须知道孩子是否处于一种更严格的结构状态。
6. 最近公共祖先
text
Info:
- foundFirst
- foundSecond
- ancestor
它不再主要统计"数值",而是在上传:
text
事件是否发生
+
答案是否已经确定
7. 最大距离
text
Info:
- height
- maxDistance
这又是经典的:
text
向上提供辅助信息 height
+
同时维护当前范围最优解 maxDistance
8. 最大快乐值
多叉树虽然没有固定的 left / right,但思路完全相同:
java
for (Employee child : employee.subordinates) {
Info childInfo = process(child);
...
}
只是把:
text
左孩子 + 右孩子
变成:
text
所有孩子
所以树形 DP 的本质从来不是"二叉树",而是:
子问题构成一棵树,每个父问题依赖孩子问题的有限状态。
第四部分:如何自己设计 Info?
遇到新题时,不要第一反应就写递归。
先问 3 个问题。
问题 1:当前节点最终要解决什么?
例如:
text
判断平衡?
求最大值?
找某个节点?
统计路径?
问题 2:为了完成这个目标,我缺什么孩子信息?
例如最大距离:
text
我要知道左边内部最大距离
我要知道右边内部最大距离
我要知道左右能向下延伸多深
于是自然得到:
text
maxDistance
height
问题 3:这些信息能否在 O(1) 时间合并?
理想状态是:
text
process(left) -> Info
process(right) -> Info
当前节点只做固定数量计算
这样每个节点只访问一次,整体就能做到:
text
O(N)
第五部分:树形 DP 的"信息设计"流程图
#mermaid-svg-5WtzhlNy0FxLpz3o{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5WtzhlNy0FxLpz3o .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5WtzhlNy0FxLpz3o .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5WtzhlNy0FxLpz3o .error-icon{fill:#552222;}#mermaid-svg-5WtzhlNy0FxLpz3o .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5WtzhlNy0FxLpz3o .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5WtzhlNy0FxLpz3o .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5WtzhlNy0FxLpz3o .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5WtzhlNy0FxLpz3o .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5WtzhlNy0FxLpz3o .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5WtzhlNy0FxLpz3o .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5WtzhlNy0FxLpz3o .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5WtzhlNy0FxLpz3o .marker.cross{stroke:#333333;}#mermaid-svg-5WtzhlNy0FxLpz3o svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5WtzhlNy0FxLpz3o p{margin:0;}#mermaid-svg-5WtzhlNy0FxLpz3o .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5WtzhlNy0FxLpz3o .cluster-label text{fill:#333;}#mermaid-svg-5WtzhlNy0FxLpz3o .cluster-label span{color:#333;}#mermaid-svg-5WtzhlNy0FxLpz3o .cluster-label span p{background-color:transparent;}#mermaid-svg-5WtzhlNy0FxLpz3o .label text,#mermaid-svg-5WtzhlNy0FxLpz3o span{fill:#333;color:#333;}#mermaid-svg-5WtzhlNy0FxLpz3o .node rect,#mermaid-svg-5WtzhlNy0FxLpz3o .node circle,#mermaid-svg-5WtzhlNy0FxLpz3o .node ellipse,#mermaid-svg-5WtzhlNy0FxLpz3o .node polygon,#mermaid-svg-5WtzhlNy0FxLpz3o .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5WtzhlNy0FxLpz3o .rough-node .label text,#mermaid-svg-5WtzhlNy0FxLpz3o .node .label text,#mermaid-svg-5WtzhlNy0FxLpz3o .image-shape .label,#mermaid-svg-5WtzhlNy0FxLpz3o .icon-shape .label{text-anchor:middle;}#mermaid-svg-5WtzhlNy0FxLpz3o .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5WtzhlNy0FxLpz3o .rough-node .label,#mermaid-svg-5WtzhlNy0FxLpz3o .node .label,#mermaid-svg-5WtzhlNy0FxLpz3o .image-shape .label,#mermaid-svg-5WtzhlNy0FxLpz3o .icon-shape .label{text-align:center;}#mermaid-svg-5WtzhlNy0FxLpz3o .node.clickable{cursor:pointer;}#mermaid-svg-5WtzhlNy0FxLpz3o .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5WtzhlNy0FxLpz3o .arrowheadPath{fill:#333333;}#mermaid-svg-5WtzhlNy0FxLpz3o .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5WtzhlNy0FxLpz3o .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5WtzhlNy0FxLpz3o .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5WtzhlNy0FxLpz3o .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5WtzhlNy0FxLpz3o .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5WtzhlNy0FxLpz3o .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5WtzhlNy0FxLpz3o .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5WtzhlNy0FxLpz3o .cluster text{fill:#333;}#mermaid-svg-5WtzhlNy0FxLpz3o .cluster span{color:#333;}#mermaid-svg-5WtzhlNy0FxLpz3o div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5WtzhlNy0FxLpz3o .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5WtzhlNy0FxLpz3o rect.text{fill:none;stroke-width:0;}#mermaid-svg-5WtzhlNy0FxLpz3o .icon-shape,#mermaid-svg-5WtzhlNy0FxLpz3o .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5WtzhlNy0FxLpz3o .icon-shape p,#mermaid-svg-5WtzhlNy0FxLpz3o .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5WtzhlNy0FxLpz3o .icon-shape .label rect,#mermaid-svg-5WtzhlNy0FxLpz3o .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5WtzhlNy0FxLpz3o .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5WtzhlNy0FxLpz3o .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5WtzhlNy0FxLpz3o :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 拿到一道树题
先定义 process 的语义
当前节点要完成什么目标?
父节点需要从孩子获得什么?
把必要内容放进 Info
设计空节点 baseInfo
递归拿到所有孩子 Info
合并出当前 Info
检查是否每个节点只计算一次
第六部分:8 道题放在一张表里
| 题目 | Info 核心字段 | 当前节点的关键判断 |
|---|---|---|
| 平衡二叉树 | balanced, height |
左右平衡且高度差 <= 1 |
| 满二叉树 | full, height |
左右满且等高 |
| BST | bst, min, max |
left.max < value < right.min |
| 最大 BST 子树 | bst, min, max, size, best |
整棵成立则取 size,否则取左右 best |
| 完全二叉树 | full, cbt, height |
按高度关系组合 |
| 最近公共祖先 | found1, found2, ancestor |
第一次同时找到两目标 |
| 最大距离 | height, maxDistance |
三种路径来源取最大 |
| 最大快乐值 | yes, no |
当前来 / 不来两种状态转移 |
第七部分:建议的验证用例
树题很容易"样例能过,边界错"。
建议至少主动覆盖下面这些结构。
1. 空树
text
null
检查每道题的对外约定和递归基线是否一致。
2. 单节点
text
1
重点检查:
- 平衡;
- 满;
- BST;
- 最大 BST 大小;
- CBT;
- 最大距离。
3. 完全左斜树
text
1
/
2
/
3
重点检查:
- 平衡;
- 满;
- CBT;
- 最大距离。
4. 有右无左
text
1
\
2
CBT 必须返回 false。
5. "直接孩子看似满足,但深层破坏 BST"
text
10
/ \
5 15
/
6
BST 必须返回 false。
6. 最近公共祖先:一个节点是另一个节点祖先
text
1
/
2
/
3
查询:
text
2 和 3
答案应该是:
text
2
7. 最近公共祖先:目标节点不存在
一个节点不在树中时,应确认你是否希望返回:
text
null
本文参考实现返回 null。
8. 最大快乐值:一条管理链
text
A(10)
|
B(100)
|
C(10)
最优答案不是简单选最大单点,而要比较隔层组合。
今日自测
1. 为什么平衡二叉树的 Info 里必须有 height?
查看答案
因为父节点除了要知道左右子树自身是否平衡,还必须比较左右高度差。仅有 isBalanced 无法完成当前节点判断。
2. Perfect Binary Tree 为什么必须要求左右子树等高?
查看答案
因为完美二叉树要求每一层都被填满。即使左右子树各自都满足"节点要么 0 个孩子要么 2 个孩子",只要高度不同,叶子就不会全部处在同一层。
3. 判断 BST 为什么需要左子树 max 和右子树 min?
查看答案
BST 的约束作用于整棵子树,而不只是直接孩子。左子树所有值都必须小于当前值,因此只需检查其中最大值;右子树所有值都必须大于当前值,因此只需检查其中最小值。
4. 最大 BST 子树中,当前整棵是 BST 时为什么要取 size?
查看答案
因为此时以当前节点为根的整棵子树就是一个合法 BST,它包含当前子树的全部节点,所以最大 BST 大小应等于当前子树总节点数,而不是某个孩子内部最大 BST 的大小再加一。
5. 使用 ArrayDeque 做 CBT 层序遍历时,为什么不能把空孩子直接入队?
查看答案
ArrayDeque 不允许 null 元素。更稳妥的实现是只入队非空节点,并用 leafMode 记录"一旦孩子不双全,之后所有节点必须是叶子"的状态。
6. 最近公共祖先为什么更推荐使用 root == first?
查看答案
题目要找的是两个具体节点。引用比较判断的是节点身份,而 equals 可能被重写为按值比较;当树中存在重复值时,按值比较可能匹配到错误节点。
7. 最大距离为什么要同时返回 height 和 maxDistance?
查看答案
maxDistance 表示子树内部已经出现的最优答案;height 则用于父节点计算"经过父节点"的新候选路径。两者承担不同职责。
8. 派对问题中,当前员工不来时为什么对子节点取 max(yes, no)?
查看答案
当前员工不参加后,不再限制直接下级是否参加,所以每个下级都可以独立选择"参加"和"不参加"中快乐值更大的状态。
加餐挑战
- 平衡二叉树实现一个"发现不平衡立即提前结束"的版本,并比较代码可读性;
- 用"节点数等于
2^height - 1"再实现一次满二叉树判断; - 用中序遍历实现 BST 判断,并比较它与 Info 模型的差异;
- 最大 BST 子树进一步返回"最大 BST 的头节点";
- CBT 写一个专门的随机树对数器,让 BFS 版本和递归版本互相验证;
- 最近公共祖先再实现"父指针 + HashSet"的解法;
- 最大距离改成"按边数"定义,并写出与"按节点数"定义之间的换算;
- 最大快乐值增加规则:"某员工来了,他的直接上级和直接下级都不能来",思考状态是否仍只需要两个。
Day 15 复盘清单
- 我能先定义
process(x)的返回语义,再开始写递归 - 我知道平衡二叉树为什么需要
height + balanced - 我能区分 Perfect Binary Tree 与"每个节点 0 或 2 个孩子"
- 我知道 BST 不能只比较直接左右孩子
- 我能解释为什么 BST 需要维护子树
min / max - 我能正确计算"最大 BST 子树"的整棵成立情况
- 我知道
ArrayDeque不能加入null - 我能独立写出 CBT 的
leafMode层序判定 - 我能解释 CBT 递归法为什么需要
full + cbt + height - 我知道 LCA 中"答案已经在子树确定"时应该继续上传
- 我会用节点引用而不是只按值识别目标节点
- 我能说清最大距离的三种答案来源
- 我会在写最大距离前先统一"节点数 / 边数"的定义
- 我能写出派对问题的
yes / no状态转移 - 我理解二叉树递归套路与树形 DP 本质上是同一类思想
- 我遇到新树题时,会先问"父节点到底需要孩子提供什么信息"
一句话总结: Day 15 真正要修炼的不是 8 份递归代码,而是"信息设计能力":把父节点需要知道的东西压缩进
Info,让每个节点只依赖孩子的有限状态;一旦Info设计正确,递归往往只是把这个定义机械地翻译成代码。