文章目录
前言
本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。
一、题目
1、原题链接
2、题目描述
二、个人思路整理
1、思路分析
思路1:哈希表映射法
具体步骤:
-
建立映射:使用哈希表
unordered_map<Node*, Node*>维护原节点 -> 新节点的一一对应关系。 -
分步构建:
- 第一轮遍历:仅创建新节点并保存数值(
val),同时存入哈希表中,不处理指针指向。 - 第二轮遍历:借助哈希表,通过原节点的指针关系直接设置新节点的指向:
- 新节点的
next指向原节点next对应的新节点:mp[cur]->next = mp[cur->next]; - 新节点的
random指向原节点random对应的新节点:mp[cur]->random = mp[cur->random]。
- 新节点的
- 第一轮遍历:仅创建新节点并保存数值(
思路2:节点交织与拆分法
无需额外哈希表,利用原链表自身的 next 指针构造"天然的映射关系":
- 复制与拼接:在每个原节点 A A A 后面紧跟插入其对应的复制节点 A ′ A' A′。
- 变换前:
A -> B -> C; - 变换后:
A -> A' -> B -> B' -> C -> C'。
- 变换前:
- 构建 random 指针:若原节点 A A A 的 random 指向 C C C(即
cur->random),则复制节点 A ′ A' A′(即cur->next)的 random 必然指向 C C C 的复制节点 C ′ C' C′(即cur->random->next)。 - 拆分链表:将交织在一起的链表分离,恢复原链表结构,同时拼接出全新的深拷贝链表。
2、解题代码
思路1:哈希表映射法
cpp
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) {
return nullptr;
}
// 用于存储 原节点->新节点 的映射关系
unordered_map<Node*, Node*> mp;
// 第一遍遍历:只创建新节点,并建立映射
Node* cur = head;
while (cur) {
mp[cur] = new Node(cur->val);
cur = cur->next;
}
// 第二遍遍历:通过哈希表设置新节点的next和random指针
cur = head;
while (cur) {
// 如果原节点的 next/random 为nullptr,mp[nullptr]默认返回 nullptr
mp[cur]->next = mp[cur->next];
mp[cur]->random = mp[cur->random];
cur = cur->next;
}
// 返回头节点对应的新节点
return mp[head];
}
};
复杂度分析
- 时间复杂度: O ( n ) O(n) O(n),遍历了两遍链表。
- 空间复杂度: O ( n ) O(n) O(n),额外维护了一个哈希表
mp。
思路2:节点交织与拆分法
cpp
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) {
return nullptr;
}
// 1. 复制每一个节点,并插入到对应原节点之后
Node* cur = head;
while (cur) {
Node* copyNode = new Node(cur->val);
copyNode->next = cur->next; // 新节点指向原节点的下一个节点
cur->next = copyNode; // 原节点指向新节点
cur = copyNode->next; // 移动到下一个原节点
}
// 2. 为复制出来的节点构造 random 指针
cur = head;
while (cur) {
if (cur->random) {
// cur->next 是新节点,cur->random->next 是random目标节点对应的新节点
cur->next->random = cur->random->next;
}
cur = cur->next->next; // 跨过复制节点,移动到下一个原节点
}
// 3. 拆分链表,恢复原链表并提取出复制链表
cur = head;
Node* dummy = new Node(0); // 虚拟头节点,方便串联新链表
Node* copyCur = dummy;
while (cur) {
Node* copyNode = cur->next;
cur->next = copyNode->next; // 恢复原链表的next指针
copyCur->next = copyNode; // 将新节点接入新链表
copyCur = copyCur->next;
cur = cur->next; // 移动到下一个原节点
}
return dummy->next;
}
};
复杂度分析
- 时间复杂度: O ( n ) O(n) O(n),三次线性遍历链表。
- 空间复杂度: O ( 1 ) O(1) O(1),若干临时指针变量空间。
三、知识风暴
哈希表映射 + 指针重建 是本题的核心思想:利用哈希表建立 原节点 -> 新节点 的一一映射,先创建所有新节点,再借助映射关系重建 next 与 random 指针,从而完成链表的深拷贝。
算法核心思想:
- 哈希表映射 :
unordered_map<Node*, Node*>维护原节点与新节点的对应关系,实现 O ( 1 ) O(1) O(1) 的指针映射查找。 - 两轮遍历 :第一轮只创建新节点并建立映射,第二轮借助映射重建
next与random指针。 - 空指针处理 :
mp[nullptr]默认返回nullptr,天然处理了next/random为空的边界情况。 - 一次深拷贝 :整个过程只需遍历链表两遍,时间复杂度为 O ( n ) O(n) O(n)。
常见对比:哈希表法 vs 节点交织法
| 方法 | 核心思路 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|---|
| 哈希表法 | 哈希表建立映射,两轮遍历重建指针 | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | 思路直观、易于理解,面试最常考察,无指针操作风险 |
| 节点交织法 | 复制节点插入原节点之后,再拆分链表 | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 空间更优,但指针操作复杂,需注意拆分时恢复原链表 |
使用要点:
- 映射建立 :第一轮遍历时
mp[cur] = new Node(cur->val),只复制数值不处理指针。 - 指针重建 :第二轮遍历时
mp[cur]->next = mp[cur->next]、mp[cur]->random = mp[cur->random],借助映射一次性完成。 - 空指针安全 :
mp[nullptr]默认返回nullptr,无需额外判断cur->next或cur->random是否为空。 - 返回头节点 :最终返回
mp[head],即原链表头节点对应的新节点。 - 递归终止条件 :
head == nullptr时直接返回nullptr,空链表无需拷贝。
算法变体与扩展:
- 深拷贝带随机指针的二叉树:将哈希表映射思想推广到二叉树,先复制节点再重建左右子树与随机指针(对应 LeetCode 1485)。
- 克隆图:借助哈希表记录已访问节点,DFS/BFS 遍历图并复制节点与邻接关系(对应 LeetCode 133)。
- 复制带随机指针的链表(递归版):用递归 + 哈希表缓存已复制节点,避免重复复制(对应 LeetCode 138 的递归解法)。
- 序列化与反序列化二叉树:借助哈希表或索引重建节点间的父子关系,与本题的映射思想异曲同工(对应 LeetCode 297)。
与其他算法的对比:
- 哈希表法 : O ( n ) O(n) O(n) 时间、 O ( n ) O(n) O(n) 空间,思路清晰、实现简单,是本题最直观的解法。
- 节点交织法 : O ( n ) O(n) O(n) 时间、 O ( 1 ) O(1) O(1) 空间,空间更优,但需注意拆分时恢复原链表结构,指针操作易出错。
相关 LeetCode 例题:
- 138. 随机链表的复制(本题,哈希表映射 + 指针重建)
- 133. 克隆图(哈希表记录已访问节点,DFS/BFS 复制)
- 1485. 克隆含随机指针的二叉树(哈希表映射推广到二叉树)
- 297. 二叉树的序列化与反序列化(映射思想重建节点关系)


