从组织架构到代码:我是如何搞定“层序遍历”这个问题的(429. N 叉树的层序遍历)

0 阅读7分钟

😎 从组织架构到代码:我是如何搞定“层序遍历”这个问题的

大家好。在咱们程序员的日常里,总有些需求听起来很简单,但做起来却发现里面藏着魔鬼。今天,我就想和大家聊聊一次我把公司“组织架构图”搬进系统的经历,以及这个过程是如何让我对“层序遍历”这个经典算法有了全新认识的。

一、我遇到了什么问题?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)来解决,不过需要一点小技巧。我们在递归时,需要带上一个“层级”信息。

解决策略:

  1. 定义一个递归函数 dfs(node, level, result)
  2. 当我们第一次到达某一个 level 时,result 列表里还没有为这一层准备好“篮子”(List<Integer>)。怎么判断是第一次呢?很简单,如果 result.size() == level,就说明我们刚到达一个新层级!这时就 new 一个新列表加到 result 里。
  3. 然后,把当前节点的值,加到对应层级的“篮子”里,即 result.get(level)
  4. 最后,递归地去处理它的所有孩子节点,并把 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)的算法(比如我们上面两种)是完全可以接受的。

五、举一反三:这个模式还能用在哪?

学会了层序遍历,你就解锁了一大类问题!

  1. 文件系统扫描:按目录深度,一层一层地列出所有文件和文件夹。
  2. 社交网络:查找“一度人脉”(直接好友)、“二度人脉”(好友的好友),这不就是典型的层序遍历嘛!
  3. 依赖解析:在npmMaven这类包管理工具中,它们需要解析项目的依赖树。层序遍历可以用来确定不同层级的依赖关系。
  4. Web爬虫:从一个种子URL开始,第一层是页面上的所有链接,第二层是这些链接指向的页面上的所有链接,以此类推。

六、更多练手机会

掌握了这个模式,不妨试试下面这些“兄弟”题目,它们的核心思想都和层序遍历有关:

希望这次的分享,能让你对“层序遍历”有一个更具体、更生动的理解。下次遇到类似“按层级”、“按距离”处理的问题时,希望你也能像我一样,自信地说出:“这不就是层序遍历嘛!”

编码愉快!🚀