从CEO到实习生:我是如何用“层序遍历”优雅地渲染公司组织架构图的(102. 二叉树的层序遍历)


👑 从CEO到实习生:一个问题,三种解法,我把公司组织架构图彻底搞明白了

嘿,各位奋斗在一线的开发者伙伴们!今天想和大家聊聊一个我们工作中大概率会遇到的场景:处理和展示层级数据。这事儿说大不大,说小不小,但优雅的解决方案和暴力硬怼之间,差的可不仅仅是几行代码,更是我们作为工程师的思维深度。

故事,要从我们公司的一次内部系统大升级说起……

我遇到了什么问题:一张“剪不断,理还乱”的组织架构图

当时,我接手了一个新任务:为公司的内部 HR 门户开发一个核心功能——一个清晰、可交互的公司组织架构图。你懂的,就是那种从创始人/CEO 开始,下面是各个副总裁(VP),再往下是总监、经理……一层一层铺开的结构。

后端给我的员工数据,天然就是一棵树形结构:每个员工对象都有一个 manager_id 指向上级。CEO 就是这棵树的根节点 (root)。

前端的同事希望我提供一个这样的 API 接口:返回一个嵌套列表,列表的每一项代表一个完整的层级,并且该层级内的员工要按固定的顺序(比如从左到右)排列。就像这样:

[
  ["CEO"],
  ["技术VP", "产品VP", "市场VP"],
  ["研发总监", "测试总监", "产品总监A", "产品总监B"],
  ...
]

拿到这种格式的数据,他们就能用 v-formap 轻松地一行一行渲染出员工卡片,布局清晰,交互流畅。

第一次尝试:陷入“递归大法”的思维定式 🤦‍♂️

我的第一反应,和许多人一样,是递归。毕竟“万物皆可递归”,处理树形结构更是它的看家本领。我自信满满地写了一个深度优先搜索(DFS)的函数:

// 一个看似正确,实则跑偏的尝试
void traverse(Employee node) {
    if (node == null) return;
    System.out.println(node.name); // 先处理自己
    for (Employee report : node.directReports) {
        traverse(report); // 再递归处理下属
    }
}

结果,API 输出的序列是这样的:CEO -> 技术VP -> 研发总监 -> 前端组长 -> ...。它会沿着一条管理链“一条路走到黑”,直到最底层的实习生,然后再回溯去走另一条分支。这完全不是前端想要的“一层一层”的数据啊!他们拿到这个扁平的、深度优先的列表,还得自己写一大堆逻辑去重组成层级结构,估计会提着键盘来找我理论。

“恍然大悟”的时刻:这不是“深度挖掘”,而是“涟漪效应”!💡

我对着那堆乱序的输出苦思冥想,突然灵光一闪。我的问题根源在于“遍历的顺序”!我需要的不是“深度优先”(Depth-First),而是“广度优先”(Breadth-First)!

想象一下往平静的湖里扔一块石头,水波是不是一圈一圈(一层一层)地向外扩散?

这不就是我想要的组织架构图的展现方式吗?!CEO 是中心,VP 们是第一圈涟漪,总监们是第二圈……而实现这种“涟漪效应”的经典算法,就是广度优先搜索(BFS),它完美地对应了算法题102. 二叉树的层序遍历

想通了这一点,我的脑海里瞬间涌现出了好几种解决方案。


解法一:教科书般的标准 BFS (队列 + Size)

这是最直观、最稳健,也是面试时最推荐的解法。它就像是修建层级大楼的“标准施工流程”。

核心工具:队列(Queue)

队列是一种“先进先出”(FIFO)的数据结构,就像排队买咖啡 ☕,最早排队的人最先拿到。这个特性完美契合我们“先处理完同一层,再处理下一层”的需求。

施工步骤:

  1. 开工:把 CEO (根节点 root) 请进一个队列里排队。
  2. 按层施工:只要队列里还有人,就循环。每一次大循环,我们只处理一整层的员工。
  3. ✨魔法时刻:如何界定“一层”?
    这是此解法的灵魂!在开始处理一个层级前,我先记录下当前队列的长度 size。这个 size 就是当前层级需要处理的总人数。这个数字像一个“快照”,帮我们把当前层和下一层完美隔离开。
  4. 处理当前层:我用一个 for 循环,精确地执行 size 次。每次从队头请出一位员工 (poll),记录他的信息,然后把他所有的直属下属 (children) 全部安排到队尾去排队 (add)。
  5. 验收:当这个 for 循环结束,当前层级的所有员工就都处理完了,而他们的下一级下属,已经整整齐齐地在队列里排好了队,等待下一轮的处理。

Talk is cheap, show me the code:

// 解法1:标准BFS,使用队列和 levelSize
class Solution1 {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();
        if (root == null) return result;

        // 我为什么用 LinkedList 作为 Queue?
        // 因为 LinkedList 实现了 Queue 接口,它提供了高效的队头移除(poll)和队尾添加(add)操作。
        // 虽然在纯性能上 ArrayDeque 可能更快,但 LinkedList 功能强大且在面试和日常使用中极为普遍。
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);

        while (!queue.isEmpty()) {
            // 关键技巧:在循环开始前锁定当前层的节点数
            int levelSize = queue.size();
            List<Integer> currentLevel = new ArrayList<>();

            for (int i = 0; i < levelSize; i++) {
                TreeNode node = queue.poll();
                currentLevel.add(node.val);
                if (node.left != null) queue.add(node.left);
                if (node.right != null) queue.add(node.right);
            }
            result.add(currentLevel);
        }
        return result;
    }
}

解法二:递归爱好者的优雅 DFS

正当我为我的 BFS 方案感到满意时,我们团队的一位资深架构师(一个递归的忠实拥趸)笑着对我说:“你知道吗,用你最初的 DFS 思想,加一点小改动,也能优雅地解决这个问题。”

核心思想:在深度优先遍历的同时,带上一个“楼层”信息。

实现步骤:

  1. 定义一个递归函数 dfs(node, level)level 参数记录当前节点在第几层。
  2. 当我们第一次到达一个新层级 level 时(此时 result.size() == level),就在 result 列表中创建一个新的空列表来代表这一层。
  3. 然后,把当前节点的值,添加到 result 中对应层级的列表里 result.get(level)
  4. 继续递归地去访问左子节点和右子节点,同时把层级 level + 1 传下去。

这种方法就像一个带着楼层探测器的“蜘蛛侠”,虽然他是在大楼里上下穿梭(DFS),但他每到一个地方,都会准确地把信息贴在对应楼层的告示板上。

// 解法2:DFS递归,传递层级 level
class Solution2 {
    public List<List<Integer>> levelOrder(TreeNode root) {
        // 使用 ArrayList 是因为它支持通过索引快速访问(get)和在末尾添加(add)新层。
        List<List<Integer>> result = new ArrayList<>();
        dfs(root, 0, result);
        return result;
    }

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

        // 关键技巧:如果第一次到达该层,则创建新列表
        if (level == result.size()) {
            result.add(new ArrayList<>());
        }
      
        // 将节点值添加到正确的层级
        result.get(level).add(node.val);

        dfs(node.left, level + 1, result);
        dfs(node.right, level + 1, result);
    }
}

解法三:巧用“哨兵”的 BFS 变体

后来,在一次代码 review 中,我又看到了一个非常 clever 的 BFS 写法。它不依赖于提前计算 size,而是用一个特殊的“哨兵”节点(比如 null)来作为层与层之间的分隔符。

实现步骤:

  1. 把 CEO (root) 和一个 null(作为第一层的结束标记)一起放入队列。
  2. 开始循环,从队列中取出一个元素。
  3. 如果取出的不是 null,说明还在当前层,就处理它,并把它的下属加入队列。
  4. 如果取出的是 null,说明当前层已经结束了!我们把收集好的当前层数据存起来,然后,如果队列里还有人(说明下一层不为空),我们就在队尾再放一个 null 作为下一层的“哨兵”。

这就像在超市收银台,每个顾客的商品之间会放一个“下一位顾客”的隔板,清晰明了。

// 解法3:BFS变体,使用 null 作为哨兵
class Solution3 {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();
        if (root == null) return result;

        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        queue.add(null); // 第一个哨兵

        List<Integer> currentLevel = new ArrayList<>();
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();

            if (node == null) { // 遇到哨兵,说明一层结束
                result.add(currentLevel);
                currentLevel = new ArrayList<>();
                // 如果队列还有节点,说明还有下一层,放入新哨兵
                if (!queue.isEmpty()) {
                    queue.add(null);
                }
            } else { // 普通节点
                currentLevel.add(node.val);
                if (node.left != null) queue.add(node.left);
                if (node.right != null) queue.add(node.right);
            }
        }
        return result;
    }
}

方案大比拼:哪种才是你的菜?

对比维度解法1 (BFS + Size)解法2 (DFS + Level)解法3 (BFS + Sentinel)
核心思想广度优先,迭代深度优先,递归广度优先,迭代
实现复杂度逻辑清晰,最标准代码简洁,但依赖递归栈写法巧妙,需处理哨兵逻辑
时间复杂度O(N)O(N)O(N)
空间复杂度O(W) (树的最大宽度)O(H) (树的高度)O(W) (树的最大宽度)

空间复杂度的抉择是关键!

  • 对于一个矮胖的树(像一个完美的金字塔),它的宽度 W 可能很大(接近 N/2),而高度 H 很小(logN)。这时,解法2 (DFS) 的空间优势巨大
  • 对于一个瘦高的树(比如一个链状的组织架构),宽度 W 很小(甚至是1),而高度 H 很大(接近 N)。这时,解法1和3 (BFS) 的空间效率更高

举一反三:这个“涟漪模式”还能用在哪?🚀

掌握了层序遍历,你等于解锁了一把能解决很多问题的万能钥匙:

  1. 社交网络的好友推荐:计算你的“二度人脉”(朋友的朋友)。你自己是第0层,直接好友是第1层,对他们进行一次层序遍历,就找到了第2层人脉。
  2. 游戏地图的最短路径:在棋盘或迷宫中,从起点开始用 BFS 逐层探索,第一次遇到终点时所经过的层数,就是最短步数。
  3. 软件依赖安装:安装一个软件包时,系统需要先安装它的直接依赖(第1层),再安装这些依赖的依赖(第2层)……BFS 可以确保所有前置依赖都被正确安装。

从一个棘手的需求,到三种各有千秋的解法,这个过程让我深刻体会到,作为开发者,我们不仅要能“实现功能”,更要能“优雅地实现功能”。希望我的这段心路历程对你有所启发!Happy coding

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值