LeetCode 430中等链表
扁平化多级双向链表 图解题解
这道题到底在问什么
每个节点有 prev、next,可能还有 child 指向另一条双向链表。按深度优先把多级链表扁平化为单层双向链表,所有 child 置空。
- 输入
- 主链 1-2-3,2.child=4-5
- 输出
- 1-2-4-5-3
- 输入
- 单节点 1,1.child=2-3
- 输出
- 1-2-3
最优解:一步一步想明白
- 3记住"遇 child 就把整条子链拉平、塞进当前节点和它的 next 之间",下面逐帧套它。
- 4开演前先看清结构:主链是 1↔2↔3,只有 3 个节点;但 2 身上挂着 child 子链 4↔5,4 又挂着 child 6。DFS 还没开始,先把整体盘一遍。
- 5正式开始:读指针从头节点 1 出发,先 examine:看 1 有没有 child。
- 6settle:节点 1 没有 child,本身已经就位。父层执行 cur = nxt 往后走,1 标记为已定型(蓝)。
- 7examine 节点 2:它挂着一条 child 子链 4↔5。先把 2 原来的后继 nxt = 3 用变量记住,免得一会儿接子链时把它弄丢。
- 82 的子链本身也是多级链,可能还带更深的 child,不能直接搬。settle 这一步先递归进 2 的子链去彻底拉平,主链 2 这里挂起等子链结果。
- 9进到 2 的子链:4↔5,读指针从头节点 4 出发。examine 4:它又挂着一条 child 子链 6。
- 10settle:4 有 child,先记住它的后继 nxt = 5,再往下递归一层去拉平 4 的子链。
- 11进到 4 的子链:只有一个节点 6。examine 6:它没有 child,是最深一层。
- 12settle:这一层不用再拆,直接返回。这条子链的头和尾都是 6,把尾 6 带回上一层。
- 13回到 4 这层做插入手术第一步:4.next 接到子链头 6、6.prev 接 4、4.child 清空。现在 4 后面挂上了 6。注意 4 原来的后继 5 这时还悬着没接回来(图里 6 和 5 之间是断开的点,5 标灰表示待接回),下一帧再补上。
- 14插入手术第二步:子链尾 tail 是 6,4 原来的后继是 5,所以现在把 6.next 接回 5、5.prev 接 6,断开的点变回箭头。这一层就拉平成 4↔6↔5。
- 15父层执行 cur = nxt 回到 5。examine 5:它没有 child,settle 直接定型,这一层走完,返回尾节点 5。
- 162 的整条子链现在彻底拉平成单层:4↔6↔5,尾节点是 5。这就是递归带回来的结果,可以拿去插进主链了。
- 17带着拉平好的子链 4↔6↔5 回到主链 2 这层。examine:目标是把子链塞进 2 和它的后继 3 之间。
- 18插入手术第一步:2.next 接到子链头 4、4.prev 接 2、2.child 清空。此刻 2 后面接上了 4↔6↔5。刚才记下的原后继 3 还等着接回来。
- 19插入手术第二步:cur 仍是节点 2,子链尾 tail 是 5。把 5.next 接回原后继 3、3.prev 接 5。3 又重新挂到了子链尾后面,2 这层就接好了:1↔2↔4↔6↔5↔3。
- 20子链 4、6、5 是在递归子调用里就处理并清空 child 的,父层不再逐个扫它们。父层执行 cur = nxt 直接回到原后继 3。examine 3:它没有 child。
- 21settle 节点 3:没有 child 直接定型,它的 next 为空,主链这一层结束。整条链已是单层、再没有任何 child。
- 22验证一下双向是否接对:从尾节点 3 沿 prev 往回走,3.prev = 5、5.prev = 6、6.prev = 4、4.prev = 2、2.prev = 1,一路顺到头,说明每处的 prev 都修对了,没有断链。
- 23扁平化完成:单层双向链表 1↔2↔4↔6↔5↔3,所有 child 都已置空。回头看,我们就是一路 DFS,遇到 child 就把整条子链拉平、塞进当前节点和它后继之间,深的子链靠递归先拉平,一遍走完。
⚠️ 容易写错的地方
✗ 错:先接子链再存 next
✓ 对:先用 nxt 存住 cur.next
改写后原后继找不回来
✗ 错:只接 next 忘了 prev
✓ 对:next 和 prev 两方向都修
prev 断了反向遍历就崩
✗ 错:忘了清空 child
✓ 对:插入后立刻 cur.child = null
题目要求结果 child 全空
✗ 错:子链当单节点搬
✓ 对:递归先彻底拉平再接
不递归会漏掉嵌套层
完整代码(Python / C++ / Java)
Python
class Node:
def __init__(self, val, prev=None, next=None, child=None):
self.val = val
self.prev = prev
self.next = next
self.child = child
class Solution:
def flatten(self, head: 'Node') -> 'Node':
def dfs(node):
cur = node
last = node
while cur:
nxt = cur.next
if cur.child:
child_head = cur.child
child_tail = dfs(child_head)
cur.next = child_head
child_head.prev = cur
cur.child = None
if nxt:
child_tail.next = nxt
nxt.prev = child_tail
last = child_tail
else:
last = cur
cur = nxt
return last
if head:
dfs(head)
return headC++
using namespace std;
class Node {
public:
int val;
Node* prev;
Node* next;
Node* child;
Node(int _val) : val(_val), prev(nullptr), next(nullptr), child(nullptr) {}
};
class Solution {
public:
Node* flatten(Node* head) {
if (head) dfs(head);
return head;
}
Node* dfs(Node* node) {
Node* cur = node;
Node* last = node;
while (cur) {
Node* nxt = cur->next;
if (cur->child) {
Node* childHead = cur->child;
Node* childTail = dfs(childHead);
cur->next = childHead;
childHead->prev = cur;
cur->child = nullptr;
if (nxt) {
childTail->next = nxt;
nxt->prev = childTail;
}
last = childTail;
} else {
last = cur;
}
cur = nxt;
}
return last;
}
};Java
class Node {
public int val;
public Node prev;
public Node next;
public Node child;
public Node(int _val) { val = _val; }
}
class Solution {
public Node flatten(Node head) {
if (head != null) dfs(head);
return head;
}
private Node dfs(Node node) {
Node cur = node, last = node;
while (cur != null) {
Node next = cur.next;
if (cur.child != null) {
Node childHead = cur.child;
Node childTail = dfs(childHead);
cur.next = childHead;
childHead.prev = cur;
cur.child = null;
if (next != null) {
childTail.next = next;
next.prev = childTail;
}
last = childTail;
} else {
last = cur;
}
cur = next;
}
return last;
}
}复杂度
时间
O(n)
每个节点恰好被访问、被拉平一次,n 是总节点数
空间
O(d)
递归栈深度等于 child 的嵌套层数 d,最坏 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 扁平化多级双向链表 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
不用递归能做吗?+
能。用一个显式栈做迭代 DFS:遍历时遇到 child,就把 cur.next 压栈、把 cur 接到 child 头、清空 child;当某节点的 next 为空且栈非空时,从栈里弹出之前存的后继接到当前尾后面。本质和递归一样,只是手动维护"待接回的后继"这个栈。
为什么是深度优先而不是广度优先?+
题目定义就是深度优先:遇到 child 要先把这条子链整个展开,再回来处理当前节点的后继。所以子链整段插在 cur 和它原后继之间,而不是排到最后。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 扁平化多级双向链表 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。