目录

题目描述

637. 二叉树的层平均值

image-20250510081805872

image-20250510081826077

题意分析

给一棵二叉树,要求返回一个 double 数组,第 i 个元素是第 i 层(根为第 0 层)所有节点值的算术平均值。答案长度等于树的高度。

求平均值意味着每一层需要两个量:这一层节点值的总和这一层的节点个数。个数不是可以事后推算的,必须在遍历时就知道边界在哪里,所以核心难点仍然是「如何把遍历切分成整齐的层」。

有两个约束会直接影响实现细节。第一,节点值范围是 $[-2^{31}, 2^{31}-1]$,而节点总数最多 $10^4$,所以一层的和可能远超 int 范围——比如 5000 个 $2^{31}-1$ 相加。累加变量必须用 64 位类型,这是本题唯一的隐藏坑,也是它虽被标为简单却值得一写的原因。第二,返回类型是浮点数,除法必须走浮点通道,整数相除会被截断。

题目还说明「与标准答案相差 $10^{-5}$ 以内视为正确」,这是在告诉你不必纠结精度控制,用 double 直接算即可,不需要任何舍入处理。

边界方面题目保证树至少有一个节点,但实现上仍应处理 root == null 返回空数组,以免入队空指针。另外树可能退化成一条链,每层只有一个节点,平均值就是节点值本身。

解法:层序遍历

核心思路

直接用队列做宽度优先遍历,弹出节点就把孩子推进去,这样访问顺序是对的,但队列里同时混着相邻两层的节点,弹出时无从判断当前节点属于哪一层,也就凑不齐「本层和」与「本层个数」这两个量。这是瓶颈。

关键观察是:在开始处理某一层之前的那个瞬间,队列里的元素恰好就是这一层的全部节点。因为上一层已经全部弹完,下一层还没被推入。于是只要在这个时刻把 queue.size() 冻结进局部变量 size,再严格只处理 size 个节点,层的边界就被精确地切开了——而且这个 size 正好就是求平均值需要的分母,一举两得。

维持的不变量是:每次进入外层循环时,队列中的元素恰好构成树的某一整层,且顺序为从左到右。初始时队列只有根节点,构成第 0 层,成立;内层循环弹出恰好 size 个节点并推入它们所有非空孩子,退出时队列里正好是下一层的全部节点,不变量得以维持。某一层不再产生孩子时队列变空,外层循环终止。

有了这个不变量,聚合就是顺带的事:内层开始前把 sum 置 0,循环中累加每个弹出节点的值,内层结束后 sum / size 就是这一层的平均值。

最后一个必须想清楚的点是类型。sum 声明为 long(Go 里 int 在 64 位平台上本就是 64 位)才能装下极端情况下的层和;而 sum / size 如果两边都是整数会做整数除法,[1, 2] 这一层会算出 1 而不是 1.5,所以要写成 sum * 1.0 / size 或显式转成浮点后再除。

解题步骤

  • 空树直接返回空列表root == null 时不存在任何层。这一步也保证了不会把 null 推进队列,避免内层取 node.val 时空指针。
  • 建队列并推入根节点:让第一轮外层循环时队列恰好是第 0 层,不变量从一开始就成立。Java 用 ArrayDeque 而非 LinkedList,常数更小;Go 用切片配合每层结束时的 queue = queue[size:] 整段截断,比逐个截断更省。
  • 外层循环条件 !queue.isEmpty():队列空说明上一层没有孩子,遍历结束。
  • 进入外层循环立刻冻结 size = queue.size():这一行是全题核心。必须在弹出任何节点之前取值并存进局部变量;写成 i < queue.size() 会因为孩子入队而不断变大,层的边界立即失效,分母也跟着错。
  • sum 声明为 long 并每层清零long 是为了容纳可能超出 int 的层和;每层清零是因为它的语义是「本层的和」,跨层累积会让第二层往后全部偏大。
  • 内层循环恰好执行 size:弹出节点、累加 node.val、把非空的左右孩子按顺序推入。判空后再入队,保证队列里永远没有 null
  • 内层结束后计算并追加 sum * 1.0 / size:乘 1.0 是为了把整数除法提升成浮点除法。追加动作必须在内层循环之外、外层循环之内,位置放错会导致每个节点各产生一个结果。
  • 返回结果列表:外层退出时所有层都已处理完。

以样例树走一遍:根为 3,左孩子 9、右孩子 20,节点 20 的左孩子 15、右孩子 7。答案应为 [3.0, 14.5, 11.0]

初始:队列 [3],结果 []

第一轮外层:size = 1sum = 0。弹出 3,sum = 3;推入 9 和 20。内层结束,追加 3 / 1 = 3.0。队列变成 [9, 20],恰好是完整的第 1 层。

第二轮外层:size = 2sum = 0。弹出 9,sum = 9,它没有孩子;弹出 20,sum = 29,推入 15 和 7。内层只跑了 2 次就停——尽管此时队列里已有 2 个新元素,冻结的 size 保证它们不会被算进本层。追加 29 / 2 = 14.5。队列变成 [15, 7]

第三轮外层:size = 2sum = 0。弹出 15(sum = 15)、弹出 7(sum = 22),两者都无孩子。追加 22 / 2 = 11.0。队列变空。

外层条件不成立,返回 [3.0, 14.5, 11.0],正确。

特别注意第二层:如果把 sum * 1.0 / size 写成 sum / sizesum 是整型,这一层会算出 14 而不是 14.5,直接判错。而如果 sumint,把这层换成两个 $2^{31}-1$ 的节点,累加会溢出成负数,平均值变成一个负的巨大数值。

代码实现

class Solution {
    // 每层结束后计算平均值并加入结果。
    public List<Double> averageOfLevels(TreeNode root) {
        List<Double> res = new ArrayList<>();
        if (root == null) {
            return res;
        }

        Deque<TreeNode> queue = new ArrayDeque<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            int size = queue.size();
            long sum = 0;

            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                sum += node.val;

                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }

            res.add(sum * 1.0 / size);
        }

        return res;
    }
}
func averageOfLevels(root *TreeNode) []float64 {
    // 每层结束后计算平均值并加入结果。
    res := make([]float64, 0)
    if root == nil {
        return res
    }

    queue := make([]*TreeNode, 0)
    queue = append(queue, root)

    for len(queue) > 0 {
        size := len(queue)
        sum := 0

        for i := 0; i < size; i++ {
            node := queue[i]
            sum += node.Val

            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }

        res = append(res, float64(sum)/float64(size))
        queue = queue[size:]
    }

    return res
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是节点总数。每个节点恰好入队一次、出队一次,出队时做一次累加和两次空判断,全是常数操作;每层额外做一次除法,层数不超过 $n$。
  • 空间复杂度:$O(w)$,其中 $w$ 是树的最大宽度,即队列长度的峰值。完全二叉树的最后一层约有 $n/2$ 个节点,所以最坏是 $O(n)$;退化成链时每层只有一个节点,只需 $O(1)$。返回数组长度等于树高,属于必要输出不计入。

关键点总结

  • 进入每轮循环时先冻结 queue.size(),是把混层的队列切成整齐层次的唯一关键;本题还额外享受一个好处——这个 size 直接就是平均值的分母,不必再单独计数。这套骨架在右视图、锯齿遍历、层最大值等题里完全通用。
  • 聚合前先问一句中间量会不会溢出:节点值可达 $2^{31}-1$ 而层宽可达数千,int 装不下层和。凡是「大量元素求和再做统计」的题,累加变量默认用 64 位是最省心的习惯。
  • 结果是浮点数时,除法两侧至少要有一侧是浮点类型;sum * 1.0 / size(double) sum / size 都行,写成 (double) (sum / size) 则是先截断后转换,等于没救。
  • 不变量「每次外层循环开始时队列恰为完整一层」比代码更值得说给面试官听,它一句话解释了内层为什么跑 size 次、新入队的孩子为什么不会被误算。
  • 本题也可以用深度优先做:递归时带层号,分别维护每层的和与计数两个数组,最后逐层相除。时间同为 $O(n)$,空间是 $O(h)$,树很宽时反而更省——能主动比较两种遍历的空间取舍是加分项。

易错点总结

  • sumint 累加:一层里有两个 $2^{31}-1$ 的节点时,累加溢出为负数,平均值变成一个负的大数,而正确答案是 $2^{31}-1$。
  • 写成 sum / size 做整数除法[1, 2] 这一层会得到 1.0 而不是 1.5,所有非整除的层都会偏小。
  • 写成 (double) (sum / size):截断已经发生,再转类型无济于事,同样把 14.5 变成 14.0。
  • 内层循环条件直接写 i < queue.size():孩子入队让上界不断变大,样例树第一轮就会把第 1 层也卷进来,sum 变成 32、size 变成 3,返回 10.67 之类的错乱值。
  • size 在弹出若干节点之后才取:层边界整体后移,样例树会把节点 9 划到下一层,结果长度和数值全错。
  • sum 忘记每层清零:第二层的和里混入了第一层,样例树会得到 [3.0, 16.0, ...],之后每层都偏大。
  • res.add(...) 写在内层循环里:每个节点各产生一个结果,样例树会返回 5 个元素而不是 3 个。
  • 忘记 root == null 的判断null 被推进队列,内层访问 node.val 立刻空指针。
  • 入队前不判断孩子是否为空null 进队后不仅下一层取值会崩,size 里还混进了不存在的节点,分母偏大导致平均值偏小。
  • Go 里用 queue = queue[1:] 逐个截断:功能正确但底层数组无法释放,深树上内存持续增长;按层整段 queue = queue[size:] 更稳妥。
  • 误以为可以先求整棵树的和再按层拆:不同层的节点在一维遍历里无法区分归属,[3,9,20][3,9,20,null,null,15,7] 的总和相同但答案完全不同。

相似题目

题目 难度 考察点
515. 在每个树行中找最大值 中等 聚合换成取最大值,初值必须用真实下界而非 0,无溢出风险
LCR 044. 在每个树行中找最大值 中等 与 515 同题
102. 二叉树的层序遍历 中等 每层输出全部节点值而非聚合成一个数,是本题骨架的最裸形态
剑指 Offer 32 - II. 从上到下打印二叉树 II 简单 与 102 同题
199. 二叉树的右视图 中等 只取每层最后一个节点,用 i == size - 1 判断即可
LCR 046. 二叉树的右视图 中等 与 199 同题
513. 找树左下角的值 中等 只要最后一层的第一个值,可先入右孩子后取最后弹出的节点
LCR 045. 找树左下角的值 中等 与 513 同题
1302. 层数最深叶子节点的和 中等 只需最后一层的和,让每层的和覆盖前一层即可,无需保存全部层
103. 二叉树的锯齿形层序遍历 中等 奇数层需反向收集,用双端队列头插比整体反转更省一次遍历
107. 二叉树的层序遍历 II 中等 层次结果自底向上,收集时头插或最后整体反转
662. 二叉树最大宽度 中等 空位也计入宽度,需给节点编号并注意深链时编号溢出
958. 二叉树的完全性检验 中等 反其道而行,空节点也入队,遇空后再出现非空即判否
429. N 叉树的层序遍历 中等 孩子数量不定,内层要遍历 children 列表而非固定左右两支
剑指 Offer 32 - I. 从上到下打印二叉树 中等 输出拉平成一维数组,反而不需要冻结 size 分层
剑指 Offer 32 - III. 从上到下打印二叉树 III 中等 与 103 同题
面试题 04.03. 特定深度节点链表 中等 每层结果要组装成链表,需在层内维护尾指针边遍历边接