😎 从组织架构到代码:我是如何搞定“层序遍历”这个问题的
大家好。在咱们程序员的日常里,总有些需求听起来很简单,但做起来却发现里面藏着魔鬼。今天,我就想和大家聊聊一次我把公司“组织架构图”搬进系统的经历,以及这个过程是如何让我对“层序遍历”这个经典算法有了全新认识的。
一、我遇到了什么问题?HR想要的“层级报告”
那是一个阳光明媚的下午,HR部门的负责人找到我,带着一个看起来很“合理”的需求:“阿飞,我们正在做一个公司人才盘点的功能,需要在系统里清晰地展示出公司的组织架构。我们不光要看到谁是谁的上级,更关键的是,要能按层级生成报告。”
她举了个例子:“比如,我需要一键导出所有‘总监’级别的人员名单,或者给所有‘一线经理’级别的人员发送一封邮件。”
我心想,这不就是棵树嘛,简单!公司的组织架构,CEO是根节点,各个副总裁是CEO的子节点,副总裁下面有总监,总监下面有经理……这是一个典型的N叉树(因为一个管理者下面可以有不止两个下属)。
需求的核心就是要**按层级(Level)**来处理数据。这让我脑海中立刻闪过了四个字:层序遍历。这不就是 LeetCode 上那道经典的 429. N 叉树的层序遍历 吗?看来,LeetCode 没白刷啊!😉
二、我是如何用[层序遍历]解决的
踩坑实录:我的第一次“想当然”尝试 🤦♂️
我的第一反应是:“递归大法好啊!” 于是我唰唰唰写了一个简单的深度优先(DFS)递归函数,想着把树打印出来就行。
// 错误示范 - 无法区分层级
void printAllEmployees_WRONG(EmployeeNode node) {
if (node == null) return;
System.out.println(node.name); // 先打印自己
for (EmployeeNode subordinate : node.subordinates) {
printAllEmployees_WRONG(subordinate); // 再打印下属
}
}
结果,打印出来的名单是这样的:CEO -> 副总裁A -> 总监A1 -> 经理A1a -> 副总裁B -> 总监B1 -> ...
HR小姐姐看了直摇头:“不对不对,我想要的是这样的报告:
- 第0层:[CEO]
- 第1层:[副总裁A, 副总裁B, ...]
- 第2层:[总监A1, 总监B1, ...]"
恍然大悟的瞬间 💡
我立刻明白了,我错在没有区分“层”。我的递归是“一条路走到黑”,而需求是“齐头并进,一层一层地走”。
这种“一层一层”的模式,简直就是为广度优先搜索(BFS)量身定做的!BFS就像在水面上扔下一颗石子,涟漪会一圈一圈地向外扩散,每一圈就是一层。这就是我需要的解决方案!
解法一:广度优先搜索 (BFS) - 最直观的“排队法”
这个方法非常贴近生活。想象一下,我们让CEO(第0层)先站出来。然后,我们让他所有的直接下属(第1层的所有人)去一个“等候区”排队。接下来,我们依次处理“等候区”里的每一个人,处理完一个,就让他所有的直接下属(第2层的人)也去“等候区”排队。如此往复,直到“等候区”没人了为止。
这个“等候区”,在我们的代码里就是队列(Queue)。
关键API和它们的作用:
Queue<Node> queue = new LinkedList<>();
- 为什么用
Queue
? 因为它符合“先进先出”(FIFO)的特性,完美模拟了“排队”的过程。先进等候区的人,先被处理。 - 为什么用
LinkedList
实现?LinkedList
在Java中是实现Queue
接口的常用类,它在队列的头部(poll
)和尾部(offer
)进行增删操作的效率非常高,都是O(1)时间复杂度。
- 为什么用
int levelSize = queue.size();
- 这是整个算法的精髓! 在我们开始处理新一层时,队列里装的,不多不少,正好是当前层的所有节点。我们先把这个数量记下来,然后只循环这么多次。这样就能完美地把每一层隔离开。
queue.offer(node);
:让一个节点进入“等候区”排队(入队)。queue.poll();
:让排在最前面的节点出列,接受处理(出队)。
代码实现:
/*
* 思路:经典的BFS层序遍历,用一个队列辅助。
* 每一轮循环处理一整层,通过预先记录 levelSize 来分隔各层。
*/
class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Queue<Node> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
// 1. 在处理新一层前,锁定当前队列大小,这就是当前层的节点数
int levelSize = queue.size();
List<Integer> currentLevel = new ArrayList<>();
// 2. 循环 levelSize 次,把当前层的所有节点都处理掉
for (int i = 0; i < levelSize; i++) {
Node currentNode = queue.poll(); // 从队列头取出一个节点
currentLevel.add(currentNode.val); // 记录它的值
// 3. 把它所有的孩子节点,加入队列末尾,为下一层做准备
queue.addAll(currentNode.children);
// addAll内部也是循环调用offer,效果一样,代码更简洁
}
// 4. 当前层处理完毕,加入最终结果
result.add(currentLevel);
}
return result;
}
}
解法二:深度优先搜索 (DFS) - 聪明的“递归法”
虽然BFS是标准答案,但我们也能用递归(DFS)来解决,不过需要一点小技巧。我们在递归时,需要带上一个“层级”信息。
解决策略:
- 定义一个递归函数
dfs(node, level, result)
。 - 当我们第一次到达某一个
level
时,result
列表里还没有为这一层准备好“篮子”(List<Integer>
)。怎么判断是第一次呢?很简单,如果result.size() == level
,就说明我们刚到达一个新层级!这时就new
一个新列表加到result
里。 - 然后,把当前节点的值,加到对应层级的“篮子”里,即
result.get(level)
。 - 最后,递归地去处理它的所有孩子节点,并把
level
加一。
代码实现:
/*
* 思路:使用DFS递归,并携带 level 信息。
* 通过比较 result.size() 和 level,巧妙地为新层级创建列表。
*/
class Solution2 {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> result = new ArrayList<>();
dfs(root, 0, result);
return result;
}
private void dfs(Node node, int level, List<List<Integer>> result) {
if (node == null) return;
// 如果当前层级是 result 列表还未触及的新深度...
if (level == result.size()) {
// ...就为这一层创建一个新的列表容器
result.add(new ArrayList<>());
}
// 将当前节点的值添加到它所属层级的列表中
result.get(level).add(node.val);
// 递归地处理所有子节点,并把层级+1传下去
for (Node child : node.children) {
dfs(child, level + 1, result);
}
}
}
这个方法代码更简洁,但理解起来需要转个弯,是不是也很有趣?😉
四、像专家一样解读“提示”
树的高度不会超过 1000
:这是给DFS解法的一颗定心丸。它告诉我,递归的最大深度是1000,这个深度对于现代计算机的调用栈来说是小菜一碟,完全不用担心“栈溢出”的问题。树的节点总数在 [0, 10^4] 之间
:这告诉我数据规模。N=10000
,意味着一个O(N)
的算法(比如我们上面两种)是完全可以接受的。
五、举一反三:这个模式还能用在哪?
学会了层序遍历,你就解锁了一大类问题!
- 文件系统扫描:按目录深度,一层一层地列出所有文件和文件夹。
- 社交网络:查找“一度人脉”(直接好友)、“二度人脉”(好友的好友),这不就是典型的层序遍历嘛!
- 依赖解析:在
npm
或Maven
这类包管理工具中,它们需要解析项目的依赖树。层序遍历可以用来确定不同层级的依赖关系。 - Web爬虫:从一个种子URL开始,第一层是页面上的所有链接,第二层是这些链接指向的页面上的所有链接,以此类推。
六、更多练手机会
掌握了这个模式,不妨试试下面这些“兄弟”题目,它们的核心思想都和层序遍历有关:
- 二叉树版:102. 二叉树的层序遍历 (梦开始的地方)
- 倒序输出:107. 二叉树的层序遍历 II (在我们的解法上加一行
Collections.reverse(result)
即可) - Z字形遍历:103. 二叉树的锯齿形层序遍历 (在遍历偶数层或奇数层时,反转一下
currentLevel
列表) - 求层级最大值:515. 在每个树行中找最大值
希望这次的分享,能让你对“层序遍历”有一个更具体、更生动的理解。下次遇到类似“按层级”、“按距离”处理的问题时,希望你也能像我一样,自信地说出:“这不就是层序遍历嘛!”
编码愉快!🚀