从 Null 到 Next:我如何用 O(1) 空间“点亮”了 UI 树的导航路径(117. 填充每个节点的下一个右侧节点指针 II)


从 Null 到 Next:我如何用 O(1) 空间"点亮"了 UI 树的导航路径 😎

嘿,各位码农同胞们!我是你们的老朋友,一个在代码世界里摸爬滚打了多年的开发者。今天,我想和大家聊聊一个我最近在项目中遇到的"小"挑战,以及我是如何从一个看似简单的需求,一路挖到 LeetCode 一道经典问题的核心,并最终用一种让我"恍然大悟"的方法解决它的。

我遇到了什么问题?🤔

想象一下,我正在开发一个功能非常复杂的企业级应用,其中有一个核心界面是一个动态的、可无限扩展的组织架构图。这个架构图用树形结构展示,每个节点代表一个部门或员工。

(它看起来有点像上面这棵树,但可能更不规则)

产品经理提出了一个需求:"为了提升用户体验,我们需要支持键盘无障碍导航 。用户应该能用 Tab 键,在同一层级的部门/员工之间从左到右依次移动焦点。当到达一行的末尾时,Tab 键应该能将焦点移到下一行的第一个节点上。"

听起来很简单,对吧?我一开始也这么觉得。这不就是个层序遍历嘛!但事情很快就变得棘手起来:

  1. 不完美的树:这个组织架构图不是一棵完美的二叉树。有的部门下面一个子部门都没有,有的有一个,有的有两个。这棵树的结构完全不可预测。
  2. 性能与内存:这个架构图可能会变得非常巨大,节点数可能达到数千个。我们的应用需要部署在一些性能和内存都相对受限的嵌入式设备上。一个占用大量内存的解决方案是绝对不可接受的。

我的任务是为每个 UI 节点(我们把它抽象成一个 Node 对象)填充一个 next 属性,让它指向右边的"兄弟"节点。这样,当用户按下 Tab 键时,我们只需要 currentNode = currentNode.next 就能瞬间完成焦点的切换,效率极高!

初次尝试:教科书式的 BFS 和它带来的"内存焦虑"😱

面对"层序"这个关键词,我的第一反应就是广度优先搜索(BFS)。这是处理树的层级问题的标准武器。

我的思路是:

  1. 用一个队列(Queue)来进行层序遍历。
  2. 每次处理一层时,先记录下当前层的节点数 levelSize
  3. 然后在一个循环里,把这一层的所有节点挨个出队。
  4. 对于每个出队的节点,我把它和队列里的下一个节点(也就是它的右邻居)连接起来。
java 复制代码
// 解法1:BFS层序遍历
public Node connect(Node root) {
    if (root == null) return null;
    Queue<Node> queue = new LinkedList<>();
    queue.offer(root);

    while (!queue.isEmpty()) {
        int levelSize = queue.size();
        Node prev = null; // 用于连接的指针
        for (int i = 0; i < levelSize; i++) {
            Node curr = queue.poll();
            if (prev != null) {
                prev.next = curr; // 连接前一个节点到当前节点
            }
            prev = curr;
            if (curr.left != null) queue.offer(curr.left);
            if (curr.right != null) queue.offer(curr.right);
        }
    }
    return root;
}

踩坑经验:这个方案在我的开发机上跑得飞快,完美地完成了任务。我当时还挺得意,觉得问题解决了。然而,当我们将它部署到目标设备上进行压力测试时,问题暴露了。对于一个特别"宽"的组织架构树,某一层的节点数可能非常多,导致队列占用了大量内存,应用响应变慢,甚至在极端情况下直接 OOM (Out of Memory) 崩溃!💥

我这才意识到,我忽略了那个"常量级额外空间"的隐性要求。BFS 的空间复杂度是 O(W),W 是树的最大宽度,在我们的场景里,这可不是常量!

恍然大悟:利用已有的 next 指针!✨

就在我一筹莫展的时候,我盯着屏幕上的代码,突然有了一个"Aha Moment"。我问自己:"我为什么需要一个队列来存储 下一层的节点呢?我能不能在遍历当前层的时候,就顺手把下一层的 next 指针链表给构建好?"

答案是肯定的!当前层的节点已经通过 next 指针被我连接成了一个链表,我完全可以利用这个链表来遍历当前层,而不需要任何额外的数据结构!

这就是 117. 填充每个节点的下一个右侧节点指针 II的精髓所在,一个空间复杂度为 O(1) 的完美解法。

这个方法的思想非常巧妙:

  1. 我们把每一层看作一个单向链表,这个链表的头节点是 leftmost
  2. 我们用一个 curr 指针遍历由 leftmost 开始的当前层链表。
  3. 在遍历的同时,我们构建下一层 的链表。为了方便操作,我们引入两个"工具人"指针:dummy(虚拟头节点,永远指向下一层链表的"前一个"位置)和 tail(下一层链表的尾指针,负责添加新节点)。
  4. 遍历 curr 时,检查它的 leftright 孩子,一旦发现非空的孩子,就把它接到 tail 的后面,并更新 tail
  5. 当前层遍历完后,dummy.next 就是下一层的头节点。我们更新 leftmost = dummy.next,然后重复这个过程,直到所有层都被处理完毕。
java 复制代码
// 解法2:O(1) 空间复杂度的完美解法
class Solution {
    public Node connect(Node root) {
        if (root == null) {
            return null;
        }
        Node leftmost = root; // 当前层的最左节点

        while (leftmost != null) {
            // dummy 是下一层的虚拟头节点,tail 是构建下一层链表的尾指针
            // 这个 dummy 节点是精髓,它避免了对下一层第一个节点的复杂判断
            Node dummy = new Node(0); 
            Node tail = dummy;
            Node curr = leftmost; // curr 用于遍历当前层

            while (curr != null) {
                if (curr.left != null) {
                    tail.next = curr.left; // 把左孩子接上
                    tail = tail.next;      // tail 后移
                }
                if (curr.right != null) {
                    tail.next = curr.right; // 把右孩子接上
                    tail = tail.next;       // tail 后移
                }
                curr = curr.next; // 沿着当前层链表移动到下一个节点
            }
            // 一层处理完了,下一层的链表也构建好了
            // 下一层的头节点就是 dummy 的下一个节点
            leftmost = dummy.next;
        }
        return root;
    }
}

这个解法简直是艺术品!它没有使用任何随输入规模增大的数据结构,只用了几个固定的指针。dummy 节点的使用是链表问题中的一个经典技巧,它让代码逻辑变得异常清晰,避免了各种 if (isFirstNodeInLevel) 的判断。这个方案不仅解决了我的内存问题,还让我对树的遍历和指针操作有了更深的理解。😉

换个角度:递归也能搞定!🧠

当然,作为一名有追求的开发者,我还会思考有没有其他解法。递归,作为解决树问题的另一大利器,当然也能登场。

提示解读:题目中特意提到"递归程序的隐式栈空间不计入额外空间复杂度",这简直是在明示我们可以尝试递归!

递归的核心是分治。connect(node) 函数的任务就是确保 node 的孩子节点们都被正确连接。

  1. 连接内部 :如果 node 有左右孩子,直接 node.left.next = node.right
  2. 连接外部 :最难的一步是连接 node 的孩子和 node.next 的孩子。我们需要找到 node 最右边的孩子,然后找到 node.next 链上第一个有孩子的节点的那个孩子,把它们连起来。
  3. 递归顺序 :这里有个大坑!我们必须先递归右子树,再递归左子树 。为什么?因为当我们在处理左子树时(比如 node.left),可能需要用到其右侧兄弟(node.right)的子孙节点的 next 信息。如果先递归左子树,右边还是"一盘散沙",信息就断了。先递归右子树,就能保证右边的 next 链总是准备就绪的。
java 复制代码
// 解法3:递归(注意递归顺序)
class Solution {
    public Node connect(Node root) {
        if (root == null) return null;
      
        Node rightmostChild = root.right != null ? root.right : root.left;

        if (root.left != null && root.right != null) {
            root.left.next = root.right;
        }

        if (rightmostChild != null) {
            Node p = root.next;
            while (p != null) {
                if (p.left != null) {
                    rightmostChild.next = p.left;
                    break;
                }
                if (p.right != null) {
                    rightmostChild.next = p.right;
                    break;
                }
                p = p.next;
            }
        }
      
        // 先右后左!先右后左!先右后左!重要的事情说三遍!
        connect(root.right);
        connect(root.left);
      
        return root;
    }
}

这个递归解法同样优雅,虽然最坏情况下的空间复杂度(递归栈深度)和 BFS 一样,但在很多场景下,树的高度 H 远小于节点总数 N,所以它依然是一个非常不错的选择。

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

掌握了这种"层序连接"的思想,你会发现它在很多地方都能派上用场:

  1. 并行计算 :在一些树形依赖的计算任务中,同一层的节点可能需要相互通信或交换数据。预先建立 next 链接,可以让这种横向通信的成本从 O(N) 降到 O(1)。
  2. 游戏 AI :在游戏中,AI 角色可能需要在网格或路点树中进行水平移动。next 指针可以帮助 AI 快速找到同一"战线"上的相邻位置。
  3. DOM 遍历优化 :在前端开发中,如果你需要频繁地在复杂的 DOM 结构中进行同级元素的导航,预处理 next 指针可以极大地提升性能。

更多练习,成为树的大师!

如果你对这类问题产生了兴趣,不妨挑战一下 LeetCode 上的这些相似题目:

希望我这次的分享能对大家有所启发。从一个实际问题出发,深入理解算法的精髓,再回归到解决问题,这个过程本身就充满了乐趣。

好了,不多说了,我得去给产品经理演示我那酷炫的 Tab 导航功能了!祝大家编码愉快,愿你的指针永远指向正确的地方!😉👋

相关推荐
㳺三才人子6 小时前
初探 Flask
后端·python·flask·html
星栈独行6 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Java爱好狂.7 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易7 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
装不满的克莱因瓶7 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl8 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
excel9 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
smj2302_796826529 小时前
解决leetcode第3943题递增后的数对数量
数据结构·python·算法·leetcode
卷毛的技术笔记9 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
炽烈小老头10 小时前
【每天学习一点算法 2026/05/25】矩阵中的最长递增路径
学习·算法·矩阵