❤️ 别怕树!一层一层剥开它的心:用BFS/DFS优雅计算层平均值
大家好,我是你们的老朋友,一名热爱分享的开发者。今天,我们不聊高深的架构,也不谈炫酷的前端,就来聊聊一个我们每天都可能间接接触到的数据结构------树,以及一个我在实际项目中遇到的,和树有关的有趣问题。
我遇到了什么问题?
想象一下,我接到了一个来自 HR 部门的需求:为我们的内部管理系统开发一个"团队健康度"分析仪表盘。公司的人员组织架构,天然就是一棵树:CEO 是根节点,下面是各个副总裁(VP),再往下是总监、经理、员工......
HR 希望能看到每一个管理层级下,员工的平均绩效得分。比如,所有 VP 这一层的平均分是多少?所有总监这一层的平均分又是多少?
这个需求翻译成技术语言就是:给定一个代表公司组织架构的二叉树(为简化问题,我们假设每个管理者最多有两个直接下属),计算出每一层节点的平均值。
这不就是 LeetCode 上的经典题目 637. 二叉树的层平均值 嘛!问题找到了,接下来就是如何优雅地解决它。
恍然大悟:用"排队"搞定层级难题
一开始,我想到的最朴素的方法就是:
- 找到第一层(只有 CEO),计算平均分。
- 找到第二层(所有 VP),计算平均分。
- 找到第三层(所有总监),计算平均分。 ...
但问题是,我怎么才能"找到某一整层的所有人"呢?这让我陷入了沉思。🤔
"恍然大悟"的瞬间来了! 我想起了去食堂打饭排队的场景。第一批进去的人打完饭,他们的"下一批"(朋友们)才接着排队进去。这不就是广度优先搜索(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^31 至 2^31 - 1 (约 ±2.1 x 10^9 ) |
-2^63 至 2^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 (推荐) |
f 或 F (必须) |
d 或 D (可选) |
无 (通过构造函数) |
核心用途 | 整数首选。循环、计数、ID等 | 当 int 不够用时(时间戳、大ID) |
内存敏感且精度要求不高的场景(如图形学) | 小数首选。科学计算、工程计算 | 必须精确计算的场景(金融、商业) |
关键注意 | 可能会溢出 | 内存占用是int 两倍 |
有精度误差,不用于精确计算 | 有精度误差,不用于精确计算 | 性能开销大,使用方法运算 |
选择原则 | "够用就行" | "int不够我再上" | "除非没内存,否则别用我" | "小数就用我" | "钱的事,交给我" |
举一反三:这个思想还能用在哪?
学会了层序遍历,你会发现它在很多地方都大有可为:
- 社交网络:分析你好友的"好友圈"。你是一级,你的直接好友是二级,好友的好友是三级... 我们可以计算出每一级好友圈的平均年龄、共同兴趣数量等。
- 游戏开发:在一个游戏中,AI 需要判断攻击的优先级。离玩家最近(第一层)的敌人威胁最大,远一点(第二层)的次之。BFS 可以帮助 AI 逐层分析战场。
- 文件系统:你想统计电脑里每个文件夹层级的平均文件大小吗?从根目录出发,用层序遍历,轻松搞定!
拓展阅读:LeetCode 上的"亲戚们"
如果你对这类问题意犹未尽,强烈推荐去挑战一下这几道题,它们的核心思想都和层序遍历息息相关:
- 102. 二叉树的层序遍历: 本题的基础版,只需要收集每一层的节点,而不用计算。
- 107. 二叉树的层序遍历 II: 102题的变体,要求从下往上返回结果。
- 103. 二叉树的锯齿形层序遍历: 更有趣的挑战,要求一层从左到右,下一层从右到左。
- 515. 在每个树行中找最大值: 和本题结构一样,只是把求平均值换成了求最大值。
好了,今天的分享就到这里。希望通过这个从实际需求出发的案例,能让你不再畏惧树形结构,并能体会到算法在解决实际问题中的乐趣和威力。下次见!👋