通俗易懂讲解"扁平化多级双向链表"算法题目
一、题目是啥?一句话说清
给定一个多级双向链表,其中节点可能包含指向子链表的指针,要求将所有层级的节点扁平化到同一层级,保持原有顺序。
示例:
- 输入:1 ↔ 2 ↔ 3 ↔ 4
│
5 ↔ 6 ↔ 7
│
8 ↔ 9 - 输出:1 ↔ 2 ↔ 5 ↔ 6 ↔ 8 ↔ 9 ↔ 7 ↔ 3 ↔ 4
二、解题核心
使用深度优先搜索(DFS),当遇到有子节点的节点时,先处理子链表,将子链表插入到当前节点和下一个节点之间,然后继续处理。
这就像整理一个多层文件夹结构,把子文件夹里的文件都拿出来,按顺序放在主文件夹中。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 深度优先遍历
- 是什么:当遇到有子节点的节点时,先深入处理子链表,再继续处理主链表。
- 为什么重要:这确保了子链表中的节点能够正确地插入到主链表中,保持深度优先的顺序。
2. 指针的正确连接
- 是什么:在插入子链表时,需要正确调整当前节点、子链表头、子链表尾和下一个节点之间的前后指针。
- 为什么重要:如果指针连接错误,会导致链表断裂或形成环,破坏链表结构。
3. 子指针的处理
- 是什么:处理完子链表后,需要将节点的子指针设置为null。
- 为什么重要:这是题目要求的一部分,确保扁平化后的链表所有子指针都为null。
四、看图理解流程(通俗理解版本)
假设多级链表如下:
1 ↔ 2 ↔ 3 ↔ 4
│
5 ↔ 6 ↔ 7
│
8 ↔ 9
-
遍历到节点2:
- 发现节点2有子节点5
- 先处理子链表:5 ↔ 6 ↔ 7(其中6有子节点8)
-
处理子链表5 ↔ 6 ↔ 7:
- 遍历到节点6,发现它有子节点8
- 先处理子子链表:8 ↔ 9
- 将8 ↔ 9插入到6和7之间:6 ↔ 8 ↔ 9 ↔ 7
- 子链表变为:5 ↔ 6 ↔ 8 ↔ 9 ↔ 7
-
将子链表插入主链表:
- 将5 ↔ 6 ↔ 8 ↔ 9 ↔ 7插入到2和3之间
- 主链表变为:1 ↔ 2 ↔ 5 ↔ 6 ↔ 8 ↔ 9 ↔ 7 ↔ 3 ↔ 4
-
设置子指针为null:
- 将节点2和节点6的子指针设置为null
五、C++ 代码实现(附详细注释)
cpp
#include <iostream>
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 == nullptr) return nullptr;
Node* current = head;
while (current != nullptr) {
// 如果当前节点有子节点
if (current->child != nullptr) {
// 保存当前节点的下一个节点
Node* next = current->next;
// 递归扁平化子链表
Node* childHead = flatten(current->child);
current->child = nullptr; // 将子指针设为null
// 将当前节点与子链表头连接
current->next = childHead;
childHead->prev = current;
// 找到子链表的尾节点
Node* childTail = childHead;
while (childTail->next != nullptr) {
childTail = childTail->next;
}
// 将子链表尾与下一个节点连接
if (next != nullptr) {
childTail->next = next;
next->prev = childTail;
}
// 移动到子链表处理后的下一个节点
current = next;
} else {
// 没有子节点,继续遍历
current = current->next;
}
}
return head;
}
};
// 辅助函数:打印双向链表
void printList(Node* head) {
Node* current = head;
while (current != nullptr) {
cout << current->val << " ";
current = current->next;
}
cout << endl;
}
// 测试代码
int main() {
// 构建示例多级链表
Node* head = new Node(1);
head->next = new Node(2);
head->next->prev = head;
head->next->next = new Node(3);
head->next->next->prev = head->next;
head->next->next->next = new Node(4);
head->next->next->next->prev = head->next->next;
// 创建子链表 5->6->7
head->next->child = new Node(5);
head->next->child->next = new Node(6);
head->next->child->next->prev = head->next->child;
head->next->child->next->next = new Node(7);
head->next->child->next->next->prev = head->next->child->next;
// 创建子子链表 8->9
head->next->child->next->child = new Node(8);
head->next->child->next->child->next = new Node(9);
head->next->child->next->child->next->prev = head->next->child->next->child;
Solution solution;
Node* result = solution.flatten(head);
printList(result); // 输出:1 2 5 6 8 9 7 3 4
// 释放内存(简单示例)
// 注意:由于链表已经扁平化,需要小心释放以避免重复释放
// 这里为了简单,不完整释放
return 0;
}
六、时间空间复杂度
- 时间复杂度:O(n),其中n是所有节点的总数(包括所有层级的节点)。每个节点被访问一次。
- 空间复杂度:O(d),其中d是链表的最大深度。这是由于递归调用栈的空间。最坏情况下,如果链表是一条链状的多级结构,d可能等于n。
七、注意事项
- 递归深度:对于深度很大的多级链表,递归可能导致栈溢出。可以考虑使用迭代方法(如栈)来避免递归。
- 指针连接顺序:在插入子链表时,要确保正确连接所有指针,包括prev和next指针。
- 子指针处理:处理完子链表后,一定要将子指针设置为null,满足题目要求。
- 边界条件:处理空链表、只有一层链表、子链表为空等情况。
- 内存管理:在C++中,如果需要释放内存,要注意链表已经扁平化,避免重复释放或遗漏释放。