LeetCode LCR 044. 在每个树行中找最大值
题目描述
题意分析
题目目标:给定一棵二叉树,按从上到下的顺序返回每一层节点值中的最大值,答案数组的长度等于树的高度,第
i个元素对应深度为i的那一层。
核心约束:答案是按"层"聚合的,节点之间只有同层才互相比较,跨层不比较,所以核心是要有一种能明确区分"当前节点属于哪一层"的遍历方式;节点数可达 $5 \times 10^4$,只能线性访问一遍。
边界处理:树可能为空,此时返回空数组而不是[0];节点值可以是负数(题目范围包含 $-2^{31}$ 附近),因此每层的初始比较基准不能取 0;只有一个节点时答案是长度为 1 的数组。
实现取舍:需要在遍历中同时掌握两件事——节点的值和它所处的层,只要能把"同一层的节点在同一批被处理"这件事做实,剩下的就只是取最大值。
解法:深度优先搜索
核心思路
最朴素的想法是先求出树高,再对每个深度单独跑一遍遍历筛出该层节点取最大值。这样要遍历
h遍,代价是 $O(nh)$,链状树退化到 $O(n^2)$,而且每一遍的绝大部分工作都是重复的。
瓶颈在于"层"这个信息被反复重建。观察到:如果按层推进遍历,即把当前层的所有节点一次性取出、处理完再把它们的孩子作为下一层,那么每个节点只被访问一次,层的归属天然就在推进过程中确定了。
由此确定不变量:每轮外层循环开始时,容器q里恰好装着当前层的全部节点,不多不少;把当前层的节点数q.size()先快照下来,用内层循环消费掉这么多个节点,消费过程中新入队的都是下一层的节点,循环结束后容器里恰好只剩下一层的全部节点,不变量对下一轮继续成立。
每一轮维护一个局部变量t表示"本层已处理节点中的最大值",初始取整型下界而不是 0,逐个用max更新;本轮结束时t就是该层答案,追加进结果数组。
解题步骤
- 先判空并直接返回空结果。为什么必须显式处理:后面的第一步是把根入队并访问它的孩子,空树入队会得到一个装着空指针的容器,取值时立刻崩溃。
- 把根节点入队,进入外层循环,条件是容器非空。为什么这个条件等价于"还有层没处理":不变量保证容器里装的就是下一个待处理层,空则说明所有层都已处理完毕。
- 每轮开始时先把
t初始化为Integer.MIN_VALUE。为什么不初始化为 0 或根值:节点值可能全为负数,取 0 会让答案错误地变成 0;取根值则跨层污染了本层的比较基准。- 用
for (int i = q.size(); i > 0; --i)固定本轮消费的节点数。为什么必须先把长度快照下来:循环体里会向同一个容器追加下一层的节点,若把q.size()直接写进循环条件里反复求值,就会把下一层的节点也卷进本层。- 每弹出一个节点,先用它的值更新
t,再把非空的左右孩子入队。为什么是"先取值后扩展":取值属于当前层的结算,扩展属于为下一层做准备,两者顺序固定才能保证不变量在轮末成立。- 内层循环结束后把
t追加进答案。为什么这一步一定在内层循环之外:只有把本层节点全部消费完,t才是这一层真正的最大值。- 以
具体用例:树[1, 3, 2, 5, 3, null, 9](根 1;第二层 3、2;第三层 5、3 是节点 3 的孩子,9 是节点 2 的右孩子)走一遍。第一轮容器为[1],快照长度 1,t从下界更新为 1,入队 3 和 2,答案得[1]。第二轮容器为[3, 2],快照长度 2,弹出 3 使t = 3并入队 5、3,弹出 2 使t仍为 3 并入队 9,答案得[1, 3]。第三轮容器为[5, 3, 9],快照长度 3,依次更新t为 5、5、9,三个节点都没有孩子,答案得[1, 3, 9]。容器变空,循环结束,返回[1, 3, 9]。
代码实现
// 核心实现:深度优先搜索,维护必要状态并避免重复处理。
class Solution {
public List<Integer> largestValues(TreeNode root) {
List<Integer> answer = new ArrayList<>();
if (root == null) {
return answer;
}
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
int t = Integer.MIN_VALUE;
for (int i = q.size(); i > 0; --i) {
TreeNode node = q.poll();
t = Math.max(t, node.val);
if (node.left != null) {
q.offer(node.left);
}
if (node.right != null) {
q.offer(node.right);
}
}
answer.add(t);
}
return answer;
}
}
// 核心实现:深度优先搜索,维护必要状态并避免重复处理。
func largestValues(root *TreeNode) []int {
var answer []int
if root == nil {
return answer
}
var q = []*TreeNode{root}
for len(q) > 0 {
t := math.MinInt32
for i := len(q); i > 0; i-- {
node := q[0]
q = q[1:]
t = max(t, node.Val)
if node.Left != nil {
q = append(q, node.Left)
}
if node.Right != nil {
q = append(q, node.Right)
}
}
answer = append(answer, t)
}
return answer
}
复杂度分析
- 时间复杂度:$O(n)$。凭什么:每个节点恰好入队一次、出队一次,出队时只做一次比较和至多两次入队判断,不存在任何节点被二次访问。
- 空间复杂度:$O(w)$,
w是树的最大层宽,最坏情况下(满二叉树的最后一层)为 $O(n)$。凭什么:容器在任意时刻至多同时存放相邻两层的部分节点,与答案数组一起构成全部额外开销。
关键点总结
- "按层聚合"类问题的通用骨架是:外层循环推进层、内层循环用快照长度消费当前层,这一层长度快照是把层边界钉死的唯一手段,换成任何"边遍历边判断深度"的写法都要额外记录深度。
- 每层的聚合变量必须在层内初始化、层末结算,作用域越紧越不容易被上一层污染,这个模式可以原样迁移到求层和、层平均、层最左值等一整类题目。
- 聚合极值时初始值要取类型下界而不是 0,凡是题目允许负数就必须这么做;若不想依赖常量,也可以用"本层第一个节点的值"作为初值。
- 空树要在入口就返回,不要指望后续循环自然跳过——把空指针挡在容器之外,比在容器内到处判空更干净。
- 面试视角:面试官通常会追问"能不能用递归做"。答案是可以:递归时把深度作为参数传下去,若
depth == answer.size()就追加一个新层的初值,否则更新已有位置;主动给出这一层等价关系,并指出递归版空间是 $O(h)$、迭代版是 $O(w)$,二者在链状树和满树上各有优势,是这题最好的加分回答。
易错点总结
- 错误写法:内层循环写成
for (int i = 0; i < q.size(); ++i)→ 树[1, 3, 2]时第一轮循环体内入队了 3 和 2 使长度变化,条件被重新求值,第二层节点被并入第一层,答案变成[3]这样的单元素数组。- 错误写法:
t初始化为 0 → 树[-1, -2, -3]的答案应为[-1, -2],实际得到[0, 0],全部负数层被 0 覆盖。- 错误写法:
t在外层循环之外只初始化一次 → 树[5, 1, 2]的答案应为[5, 2],实际第二层继承了上一层的 5,得到[5, 5]。- 错误写法:漏掉
root == null的判空 → 空树输入时把null入队,出队后访问node.val直接空指针异常。- 错误写法:把空孩子也入队,靠出队时判空跳过 → 空节点被计入本层长度快照,
t的更新逻辑要额外分支,一旦忘记就在访问node.val时崩溃;即使不崩,某些实现里空层还会多产出一个答案项。- 错误写法:
answer.add(t)写在内层循环里面 → 树[1, 3, 2]时第二层产出两次,答案变成[1, 3, 3],长度不等于树高。- 错误写法:Go 中用
math.MinInt32却假设节点值可能小于它 → 若变种题把值域放宽到 64 位,初值不够小会让整层答案错误,应改用math.MinInt64或首元素初始化。- 错误写法:先扩展孩子再用节点值更新
t,同时把t的更新写成"用队首元素" → 队首此时已经是下一层的节点,树[1, 3, 2]的第一层答案被算成 3。- 错误写法:用一个全局
List按深度下标直接set却没先扩容 → 递归改写时对尚不存在的下标调用set抛出越界异常,必须先判断depth == answer.size()再add。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 102. 二叉树的层序遍历 | 中等 | 每层结果是完整列表而非聚合值,考察分层收集 |
| 103. 二叉树的锯齿形层序遍历 | 中等 | 在分层基础上按奇偶层反转方向 |
| 199. 二叉树的右视图 | 中等 | 每层只取末位元素,聚合规则换成位置而非大小 |
| 637. 二叉树的层平均值 | 简单 | 聚合量变成和与个数,还要注意求和的溢出 |
| 662. 二叉树最大宽度 | 中等 | 需要给节点附带层序编号才能算上空位的宽度 |
| 1302. 层数最深叶子节点的和 | 中等 | 只关心最后一层的聚合结果,可用覆盖式写法简化 |