别怕树!一层一层剥开它的心:用BFS/DFS优雅计算层平均值(637. 二叉树的层平均值)


❤️ 别怕树!一层一层剥开它的心:用BFS/DFS优雅计算层平均值

大家好,我是你们的老朋友,一名热爱分享的开发者。今天,我们不聊高深的架构,也不谈炫酷的前端,就来聊聊一个我们每天都可能间接接触到的数据结构------,以及一个我在实际项目中遇到的,和树有关的有趣问题。

我遇到了什么问题?

想象一下,我接到了一个来自 HR 部门的需求:为我们的内部管理系统开发一个"团队健康度"分析仪表盘。公司的人员组织架构,天然就是一棵树:CEO 是根节点,下面是各个副总裁(VP),再往下是总监、经理、员工......

HR 希望能看到每一个管理层级下,员工的平均绩效得分。比如,所有 VP 这一层的平均分是多少?所有总监这一层的平均分又是多少?

这个需求翻译成技术语言就是:给定一个代表公司组织架构的二叉树(为简化问题,我们假设每个管理者最多有两个直接下属),计算出每一层节点的平均值。

这不就是 LeetCode 上的经典题目 637. 二叉树的层平均值 嘛!问题找到了,接下来就是如何优雅地解决它。

恍然大悟:用"排队"搞定层级难题

一开始,我想到的最朴素的方法就是:

  1. 找到第一层(只有 CEO),计算平均分。
  2. 找到第二层(所有 VP),计算平均分。
  3. 找到第三层(所有总监),计算平均分。 ...

但问题是,我怎么才能"找到某一整层的所有人"呢?这让我陷入了沉思。🤔

"恍然大悟"的瞬间来了! 我想起了去食堂打饭排队的场景。第一批进去的人打完饭,他们的"下一批"(朋友们)才接着排队进去。这不就是广度优先搜索(BFS) 的核心思想吗?用一个队列(Queue),完美模拟"逐层处理"的过程!

解法1:广度优先搜索(BFS)------ 最直观的"排队法"

BFS 的思路和咱们的需求简直是天作之合。

<思路> 我们用一个队列来存放需要访问的节点。每一轮循环,我们都只处理当前队列里"存着"的所有节点,这些人就构成了"一层"。我们把他们的绩效分加起来,然后除以人数,就得到了这一层的平均分。在处理他们的时候,再把他们的直接下属(子节点)放到队列里,为下一轮循环做准备。 </思路>

java 复制代码
/*
 * 思路:BFS层序遍历。使用队列模拟逐层访问,每一轮循环处理一整层。
 * 时间复杂度:O(N),每个员工(节点)都只入队和出队一次。
 * 空间复杂度:O(W),W是公司最"胖"的那一层的人数(树的最大宽度)。
 */
import java.util.*;

class Solution {
    public List<Double> averageOfLevels(TreeNode root) {
        List<Double> averages = new ArrayList<>();
        if (root == null) return averages;

        // 为何用 Queue?因为它"先进先出"的特性,完美匹配我们一层一层处理的需求。
        // LinkedList 是实现 Queue 接口的常用类,提供了 offer(入队) 和 poll(出队) 方法。
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root); // CEO 先入队

        while (!queue.isEmpty()) {
            // 关键点1:在循环开始前,记下当前层的人数!
            int levelSize = queue.size();
            // 关键点2:"踩坑"经验!员工绩效分加起来可能很大,超过int范围。
            // 必须用 long 来存总和,否则计算结果可能溢出变成负数!我真的错过一次!😱
            long levelSum = 0L;

            for (int i = 0; i < levelSize; i++) {
                TreeNode current = queue.poll(); // 让当前层的员工出队
                levelSum += current.val;

                // 把他的下属(子节点)加入队列,让他们在下一轮排队
                if (current.left != null) queue.offer(current.left);
                if (current.right != null) queue.offer(current.right);
            }

            // 计算平均分,记得转成 double 做浮点数除法
            averages.add((double) levelSum / levelSize);
        }
        return averages;
    }
}
解法2:深度优先搜索(DFS)------ "递归"的别样风情

我把 BFS 的方案给同事看,他是个递归的忠实粉丝,他问:"用递归能搞定吗?" 当然可以!这就是深度优先搜索(DFS)

<思路> DFS 的思路是"一条路走到黑"。为了统计每一层的数据,我们需要在递归函数里多传递一个参数 level,告诉当前节点它在第几层。同时,我们需要两个全局的列表,一个 sums 存每层的总分,一个 counts 存每层的人数。列表的索引就天然对应了层级。 </思路>

java 复制代码
/*
 * 思路:DFS递归。通过递归函数传递层级level,将每层的数据聚合到外部的列表中。
 * 时间复杂度:O(N),每个员工还是只访问一次。
 * 空间复杂度:O(H),H是公司的管理层级深度(树的高度)。
 */
class Solution {
    public List<Double> averageOfLevels(TreeNode root) {
        List<Long> sums = new ArrayList<>();   // 索引i存放第i层的总分
        List<Integer> counts = new ArrayList<>(); // 索引i存放第i层的人数
        dfs(root, 0, sums, counts);

        List<Double> averages = new ArrayList<>();
        for (int i = 0; i < sums.size(); i++) {
            averages.add((double) sums.get(i) / counts.get(i));
        }
        return averages;
    }

    private void dfs(TreeNode node, int level, List<Long> sums, List<Integer> counts) {
        if (node == null) return;

        // 如果是第一次到达这一层
        if (level == sums.size()) {
            // 就给这一层新增一个"账本"
            sums.add((long) node.val);
            counts.add(1);
        } else {
            // 如果这一层的账本已存在,就在上面更新
            sums.set(level, sums.get(level) + node.val);
            counts.set(level, counts.get(level) + 1);
        }

        // 递归地去访问下属
        dfs(node.left, level + 1, sums, counts);
        dfs(node.right, level + 1, sums, counts);
    }
}

解读一下题目的"提示"

  • 树中节点数量在 [1, 10^4] 范围内:这告诉我们,一个 O(N) 的算法是绰绰有余的,我们不需要去想什么花里胡哨的骚操作。BFS 和 DFS 都是 O(N),完美!
  • -2^31 <= Node.val <= 2^31 - 1:这是最关键的提示,也是最容易掉进去的坑!它暗示了单层节点值的总和有可能会超出 int 的表示范围 。如果我们用 int 来累加,当一层有几千个节点,且节点值都很大时,就会发生整数溢出 ,得到一个错误的结果。所以,必须使用 long 来作为累加器!

Java 数值类型核心特性总结表

特性 int long float double BigDecimal (类)
类型 32位整数 64位整数 32位单精度浮点数 64位双精度浮点数 任意精度十进制数
大小 4字节 8字节 4字节 8字节 可变
范围/精度 -2^312^31 - 1 (约 ±2.1 x 10^9) -2^632^63 - 1 (约 ±9.2 x 10^18) 范围 : 约 ±3.4 x 10^38 精度: 6-7位有效数字 范围 : 约 ±1.8 x 10^308 精度: 15-17位有效数字 精度和范围理论上无限,仅受限于内存
默认值 0 0L 0.0f 0.0d null
字面量后缀 L (推荐) fF (必须) dD (可选) 无 (通过构造函数)
核心用途 整数首选。循环、计数、ID等 int 不够用时(时间戳、大ID) 内存敏感且精度要求不高的场景(如图形学) 小数首选。科学计算、工程计算 必须精确计算的场景(金融、商业)
关键注意 可能会溢出 内存占用是int两倍 有精度误差,不用于精确计算 有精度误差,不用于精确计算 性能开销大,使用方法运算
选择原则 "够用就行" "int不够我再上" "除非没内存,否则别用我" "小数就用我" "钱的事,交给我"

举一反三:这个思想还能用在哪?

学会了层序遍历,你会发现它在很多地方都大有可为:

  1. 社交网络:分析你好友的"好友圈"。你是一级,你的直接好友是二级,好友的好友是三级... 我们可以计算出每一级好友圈的平均年龄、共同兴趣数量等。
  2. 游戏开发:在一个游戏中,AI 需要判断攻击的优先级。离玩家最近(第一层)的敌人威胁最大,远一点(第二层)的次之。BFS 可以帮助 AI 逐层分析战场。
  3. 文件系统:你想统计电脑里每个文件夹层级的平均文件大小吗?从根目录出发,用层序遍历,轻松搞定!

拓展阅读:LeetCode 上的"亲戚们"

如果你对这类问题意犹未尽,强烈推荐去挑战一下这几道题,它们的核心思想都和层序遍历息息相关:


好了,今天的分享就到这里。希望通过这个从实际需求出发的案例,能让你不再畏惧树形结构,并能体会到算法在解决实际问题中的乐趣和威力。下次见!👋

相关推荐
我爱一条柴ya4 分钟前
【AI大模型】线性回归:经典算法的深度解析与实战指南
人工智能·python·算法·ai·ai编程
三维重建-光栅投影2 小时前
VS中将cuda项目编译为DLL并调用
算法
课堂剪切板4 小时前
ch03 部分题目思路
算法
山登绝顶我为峰 3(^v^)35 小时前
如何录制带备注的演示文稿(LaTex Beamer + Pympress)
c++·线性代数·算法·计算机·密码学·音视频·latex
Two_brushes.6 小时前
【算法】宽度优先遍历BFS
算法·leetcode·哈希算法·宽度优先
森焱森8 小时前
水下航行器外形分类详解
c语言·单片机·算法·架构·无人机
QuantumStack10 小时前
【C++ 真题】P1104 生日
开发语言·c++·算法
写个博客11 小时前
暑假算法日记第一天
算法
绿皮的猪猪侠11 小时前
算法笔记上机训练实战指南刷题
笔记·算法·pta·上机·浙大
hie9889411 小时前
MATLAB锂离子电池伪二维(P2D)模型实现
人工智能·算法·matlab