一、题目描述
给定一个链表,每个节点包含:
val:节点值
next:指向下一个节点
random:随机指针,可以指向链表中的任意节点或 null
要求构造深拷贝:
- 复制链表中的每个节点要是全新节点
next 和 random 都要指向复制链表中的新节点
- 原链表和复制链表的指针状态要完全一致
注意: 复制链表中的指针不能指向原链表的节点。
二、解题方法总览
| 方法 |
核心思路 |
时间复杂度 |
空间复杂度 |
| 方法一:哈希表法 |
用哈希表记录 原节点 → 新节点 的映射 |
O(n) |
O(n) |
| 方法二:原地插入法 |
把复制节点插入原节点后面,三步完成 |
O(n) |
O(1) |
| 方法三:DFS 深搜法 |
用递归 + 哈希表处理 random 链 |
O(n) |
O(n) |
三、方法一:哈希表法(最易理解)
3.1 核心思想
两次遍历:
- 第一次:遍历原链表,创建所有新节点,存入
hash[原节点] = 新节点
- 第二次:遍历原链表,从哈希表中查找新节点,设置
next 和 random
3.2 流程图
复制代码
┌─────────────────────────────────────────┐
│ 开始 │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 第一次遍历:遍历原链表 │
│ 对每个原节点 cur: │
│ 创建新节点 new Node(cur->val) │
│ hash[cur] = new │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 第二次遍历:遍历原链表 │
│ 对每个原节点 cur: │
│ new->next = hash[cur->next] │
│ new->random = hash[cur->random] │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 返回 hash[head] │
└──────────────┬──────────────────────────┘
▼
【结束】
3.3 代码实现
cpp
复制代码
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) return nullptr;
unordered_map<Node*, Node*> hash;
// ========== 第一次遍历:创建新节点 ==========
Node* cur = head;
while (cur) {
hash[cur] = new Node(cur->val);
cur = cur->next;
}
// ========== 第二次遍历:设置指针 ==========
cur = head;
while (cur) {
hash[cur]->next = hash[cur->next]; // cur->next 可能是 nullptr
hash[cur]->random = hash[cur->random]; // cur->random 可能是 nullptr
cur = cur->next;
}
return hash[head];
}
};
3.4 图解示例
复制代码
【原链表】
1 --→ 2 --→ 3 --→ null
↑ ↑
└──────────┘ random
【哈希表】
1 → 1' 2 → 2' 3 → 3'
【复制链表】
1' --→ 2' --→ 3' --→ null
↑ ↑
└──────────┘ random
3.5 复杂度分析
| 指标 |
复杂度 |
说明 |
| 时间 |
O(n) |
两次遍历 |
| 空间 |
O(n) |
哈希表存储 n 个节点的映射 |
四、方法二:原地插入法(最优空间)
4.1 核心思想
利用原链表的空间,把新节点直接插入原节点后面:
- Step 1:在每个原节点后插入一个复制节点
- Step 2 :设置复制节点的 random(利用
原节点.random.next)
- Step 3:拆分新旧链表,恢复原链表
4.2 完整流程图
复制代码
┌─────────────────────────────────────────┐
│ 开始 │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ Step 1:插入复制节点 │
│ for cur = head; cur; cur = cur->next->next│
│ cur->next = new Node(cur->val) │
│ new->next = cur->next │
└──────────────┬──────────────────────────┘
▼
【链表变化示意】
插入前: 1 → 2 → 3 → null
插入后: 1 → 1' → 2 → 2' → 3 → 3' → null
▼
┌─────────────────────────────────────────┐
│ Step 2:设置 random │
│ for cur = head; cur; cur = cur->next->next│
│ if (cur->random) │
│ cur->next->random = cur->random->next│
└──────────────┬──────────────────────────┘
▼
【random 变化示意】
原节点 1.random → 3
→ 1'.random → 3.next = 3'
▼
┌─────────────────────────────────────────┐
│ Step 3:拆分链表 │
│ 创建 dummy -> tail │
│ for cur = head; cur; cur = cur->next│
│ tail->next = cur->next │
│ cur->next = cur->next->next │
│ tail = tail->next │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 原链表恢复: 1 → 2 → 3 → null │
│ 新链表: 1' → 2' → 3' → null │
│ 返回 dummy->next │
└──────────────┬──────────────────────────┘
▼
【结束】
4.3 代码实现
cpp
复制代码
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) return nullptr;
// ========== Step 1:插入复制节点 ==========
// 在每个原节点后插入一个复制节点
// 1 -> 1' -> 2 -> 2' -> 3 -> 3'
for (Node* cur = head; cur; cur = cur->next->next) {
cur->next = new Node(cur->val, cur->next, NULL);
}
// ========== Step 2:设置 random ==========
// 复制节点的 random = 原节点 random 的下一个
for (Node* cur = head; cur; cur = cur->next->next) {
if (cur->random) {
cur->next->random = cur->random->next;
}
}
// ========== Step 3:拆分链表 ==========
// 用 tail 尾插法构建新链表,同时恢复原链表
Node* dummy = new Node(0);
Node* tail = dummy;
for (Node* cur = head; cur; cur = cur->next) {
Node* copy = cur->next;
tail->next = copy; // 新链表尾插
cur->next = copy->next; // 恢复原链表
tail = tail->next;
}
return dummy->next;
}
};
4.4 完整图解示例
初始链表
复制代码
节点: 1 2 3
val: 7 13 11
random: null 1 4(null)
next: 2 3 null
Step 1 后:插入复制节点
复制代码
1 → [1'] → 2 → [2'] → 3 → [3']
其中:
- 1'.val = 7
- 2'.val = 13
- 3'.val = 11
Step 2 后:设置 random
复制代码
1.random = null → 1'.random = null
2.random = 1 → 2'.random = 1.next = 1'
3.random = null → 3'.random = null
Step 3 后:拆分
复制代码
原链表: 1 → 2 → 3 → null (已恢复)
新链表: 1' → 2' → 3' → null (目标答案)
4.5 复杂度分析
| 指标 |
复杂度 |
说明 |
| 时间 |
O(n) |
三次遍历 |
| 空间 |
O(1) |
只用了几个指针变量 |
五、方法三:DFS 深搜法(递归思维)
5.1 核心思想
用递归处理链表的 random 链:
- 递归遍历链表,遇到一个新节点就创建它
- 递归处理
next 和 random 两条链
- 用哈希表记录 visited,防止重复创建
5.2 流程图
复制代码
┌─────────────────────────────────────────┐
│ copyRandomList(head) │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ if head == nullptr → return nullptr │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ if hash contains head → return it │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 创建 newNode = new Node(head->val) │
│ hash[head] = newNode │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ newNode->next = DFS(head->next) │
│ newNode->random = DFS(head->random) │
│ return newNode │
└──────────────┬──────────────────────────┘
▼
【返回新链表头节点】
5.3 代码实现
cpp
复制代码
class Solution {
public:
unordered_map<Node*, Node*> hash;
Node* copyRandomList(Node* head) {
if (!head) return nullptr;
if (hash.count(head)) return hash[head];
Node* newNode = new Node(head->val);
hash[head] = newNode;
newNode->next = copyRandomList(head->next);
newNode->random = copyRandomList(head->random);
return newNode;
}
};
5.4 递归展开示意
复制代码
copyRandomList(1)
→ 创建 1'
→ 1'.next = copyRandomList(2)
→ 1'.random = copyRandomList(null) → null
→ 返回 1'
其中 copyRandomList(2):
→ 创建 2'
→ 2'.next = copyRandomList(3)
→ 2'.random = copyRandomList(1) → hash[1] = 1'
→ 返回 2'
其中 copyRandomList(3):
→ 创建 3'
→ 3'.next = copyRandomList(null) → null
→ 3'.random = copyRandomList(null) → null
→ 返回 3'
5.5 复杂度分析
| 指标 |
复杂度 |
说明 |
| 时间 |
O(n) |
每个节点访问一次 |
| 空间 |
O(n) |
哈希表 + 递归栈深度 |
六、三种方法对比
6.1 横向对比表
| 维度 |
方法一 哈希表 |
方法二 原地插入 |
方法三 DFS |
| 代码复杂度 |
⭐⭐⭐ 清晰直观 |
⭐⭐ 需三步操作 |
⭐⭐ 递归需理解 |
| 时间复杂度 |
O(n) |
O(n) |
O(n) |
| 空间复杂度 |
O(n) |
O(1) ✅ |
O(n) |
| 面试推荐度 |
⭐⭐⭐ 思路清晰 |
⭐⭐⭐ 最优解 |
⭐⭐ 回溯思维 |
| 适用场景 |
工程实现 |
面试空间优化 |
有向图问题延伸 |
6.2 核心差异图
复制代码
【方法一:哈希表法】
原链表: 1 → 2 → 3
↓
┌──────────┐
↓ ↓
1' ← 2' ← 3' (独立存在,靠哈希表维护关系)
【方法二:原地插入法】
插入后: 1 → 1' → 2 → 2' → 3 → 3'
拆分后: 1' → 2' → 3' (独立存在)
【方法三:DFS 递归】
原链表: 1 → 2 → 3
↓
┌──────────┐
↓ ↓
1' ← 2' ← 3' (通过递归链维护关系)
6.3 面试选择建议
| 情况 |
推荐方法 |
原因 |
| 面试被问到最优空间 |
方法二 |
唯一 O(1) 空间解法 |
| 想写清晰易懂代码 |
方法一 |
两遍遍历,逻辑清晰 |
| random 链复杂(如环) |
方法三 |
递归天然适合处理有向依赖 |
七、常见面试追问
| 问题 |
回答要点 |
| 为什么方法二的空间是 O(1)? |
只用了 dummy、tail、cur 三个指针,没有额外容器 |
Step 2 为什么 cur->random->next 一定是新节点? |
Step 1 已将新节点插入原节点后面,原节点.random 的 next 就是对应新节点 |
| Step 3 为什么要恢复原链表? |
题目要求原链表保持不变,新链表独立存在 |
| random 指向 null 怎么办? |
用 if (cur->random) 判断,为 null 时不设置,默认就是 null |
| 方法三的递归栈会爆吗? |
n ≤ 1000,栈深度可控;工程中建议用方法一或方法二 |
| 三种方法本质相同点? |
都用哈希表记录映射关系;方法二将哈希表「隐式」在 next 链中 |
八、完整代码汇总
cpp
复制代码
// ========== 方法一:哈希表法 ==========
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) return nullptr;
unordered_map<Node*, Node*> hash;
Node* cur = head;
while (cur) {
hash[cur] = new Node(cur->val);
cur = cur->next;
}
cur = head;
while (cur) {
hash[cur]->next = hash[cur->next];
hash[cur]->random = hash[cur->random];
cur = cur->next;
}
return hash[head];
}
};
// ========== 方法二:原地插入法 ==========
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) return nullptr;
// Step 1: 插入复制节点
for (Node* cur = head; cur; cur = cur->next->next) {
cur->next = new Node(cur->val, cur->next, NULL);
}
// Step 2: 设置 random
for (Node* cur = head; cur; cur = cur->next->next) {
if (cur->random) {
cur->next->random = cur->random->next;
}
}
// Step 3: 拆分链表
Node* dummy = new Node(0);
Node* tail = dummy;
for (Node* cur = head; cur; cur = cur->next) {
Node* copy = cur->next;
tail->next = copy;
cur->next = copy->next;
tail = tail->next;
}
return dummy->next;
}
};
// ========== 方法三:DFS 递归法 ==========
class Solution {
public:
unordered_map<Node*, Node*> hash;
Node* copyRandomList(Node* head) {
if (!head) return nullptr;
if (hash.count(head)) return hash[head];
Node* newNode = new Node(head->val);
hash[head] = newNode;
newNode->next = copyRandomList(head->next);
newNode->random = copyRandomList(head->random);
return newNode;
}
};
九、总结
| 要点 |
内容 |
| 核心难点 |
random 可能指向尚未创建的节点 |
| 方法一 |
哈希表维护映射,两遍遍历,O(n) 空间 |
| 方法二 |
原地插入 + 拆分,O(1) 空间,面试最优解 |
| 方法三 |
递归 + 哈希表,思维巧妙,适合延伸 |
| 记忆口诀 |
「插、设、分」三步走,拿下原地复制法 |