LeetCode 430:扁平化多级双向链表的递归与迭代解法详解

1. 项目概述:当链表有了“子节点”

如果你刷过一些链表题,可能会觉得链表无非就是 val next ,顶多再加个 prev 变成双向链表。但 Leetcode 430 这道“扁平化多级双向链表”的题目,直接把复杂度提升了一个维度:它引入了一个叫 child 的指针。这不再是简单的“一条线”,而是一棵可能在任何节点分叉的“树”,只不过这棵树是用链表节点连起来的。

想象一下,你手头有一份文档的大纲,主标题(一级节点)下可能有子标题(二级节点),子标题下还有更细的说明(三级节点)。这份大纲在内存里就是用这种带 child 指针的链表存储的。现在,老板要求你把这份“层级式”的大纲,转换成一份“扁平化”的、所有内容按深度优先顺序依次排列的纯文本列表。Leetcode 430 要解决的就是这个问题:遍历这棵“链表树”,按照深度优先的顺序,将所有的 child 链表“拉平”,并插入到当前节点和它的下一个节点之间,最终形成一个单一、绵长的双向链表。

这题之所以被频繁讨论(从相关热词如“链表遍历”、“链表插入”的高频出现可见一斑),是因为它完美融合了链表的基础操作(遍历、插入、指针修改)和树的深度优先搜索(DFS)思想。它考察的不仅仅是你会不会写 while 循环移动指针,更是考察你能否在复杂的指针关系变化中保持清晰的逻辑,处理好 prev next child 这三个指针的“牵一发而动全身”。很多朋友在初次尝试时,很容易被绕晕,导致链表断裂或者形成环。接下来,我就结合自己多次调试和教学的经验,把这道题的“里子”和“面子”都拆解清楚。

2. 核心思路拆解:递归与迭代的抉择

面对这种具有“层级”或“嵌套”结构的问题,我们的第一反应往往是递归。因为递归的思想天然契合“深度优先”:处理当前节点,如果它有孩子,就一头扎进去处理孩子链表,处理完了再回来继续。这题用递归实现确实非常直观。

2.1 递归解法:清晰但需注意细节

递归函数 dfs(node) 的核心职责是:扁平化以 node 为头节点的链表(及其所有子链表),并返回扁平化后的 尾节点 。返回尾节点是关键,因为父节点需要知道子链表处理完后,应该接回哪里。

递归过程可以分解为以下几步:

  1. 初始化 :定义 current 指针指向当前节点, tail 指针用于记录当前链表的最后一个节点,初始化为 current
  2. 循环处理 :只要 current 不为空,就持续处理。
  3. 保存后继 :这是极易出错的一步!在处理 current child 之前,必须先用一个临时变量 next_node 保存 current.next 。因为一旦我们开始处理 child current.next 会被修改指向子链表的头节点,原来的后继关系就丢失了。
  4. 处理子链表 :如果 current.child 不为空:
    • 递归调用 dfs(current.child) ,得到子链表扁平化后的尾节点 child_tail
    • 重新接线
      • current.next 指向 current.child
      • current.child.prev 指向 current
      • 这是将子链表“拉出来”接到主链上的关键操作。
    • 清空 child 指针 :题目要求扁平化后所有 child 指针都应置为 null
    • 连接子链表尾部 :将 child_tail.next 指向我们之前保存的 next_node
    • 如果 next_node 不为空(即当前节点不是原链表的最后一个节点),需要将 next_node.prev 指向 child_tail
    • 更新 tail :此时,整个链表(包含刚接入的子链表)的尾节点变成了 child_tail (或者如果 child_tail 后面还有 next_node ,则 tail 会在后续循环中更新,但这里 tail 应更新为 child_tail 以确保递归返回正确的尾节点)。
  5. 移动指针 :无论是否有 child ,都将 current 移动到它的 next 节点(注意,此时的 next 可能已经是子链表的头节点了),并更新 tail current (当 current 不为空时)。
  6. 返回尾节点 :当 current 为空,循环结束,返回 tail

注意 :递归解法在逻辑上清晰,但需要警惕栈溢出风险。虽然本题的测试数据通常不会让递归深度达到溢出程度,但在工业级代码或层级极深的情况下,迭代解法是更安全的选择。

2.2 迭代解法:模拟栈的深度优先遍历

迭代解法的核心思想是 显式地使用一个栈(Stack)来模拟递归的调用过程 。我们沿着 next 指针一路向前,当遇到有 child 的节点时,我们并不立即深入,而是把当前节点的“未来”(即 next 节点)先压入栈中保存起来,然后转向处理 child 。等 child 这条分支处理完了,再从栈里把之前保存的“未来”弹出来继续处理。

具体步骤:

  1. 创建一个栈 stack ,初始化当前指针 curr = head
  2. while 循环,只要 curr 不为空,或者栈不为空(还有未处理的分支),就继续。
  3. 如果 curr.child
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值