一、题目描述
138. 随机链表的复制
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝 。深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点。
返回复制链表的头节点。
示例 1:
输入:
head = [[7,null],[13,0],[11,4],[10,2],[1,0]]输出:
[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:
输入:
head = [[1,1],[2,1]]输出:
[[1,1],[2,1]]
示例 3:
输入:
head = [[3,null],[3,0],[3,null]]输出:
[[3,null],[3,0],[3,null]]
提示:
-
0 <= n <= 1000 -
-10⁴ <= Node.val <= 10⁴ -
random指针为null或指向链表中的有效节点。
进阶: 你能用 O(1) 空间解决此问题吗?
二、解题思路概览
本题难点在于随机指针的复制:在创建新节点时,原节点的 random 指针可能指向尚未创建的节点,因此无法一次遍历完成。常见解法有三种:
| 解法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 哈希表法(两次遍历) | O(n) | O(n) | 最直观,易于理解 |
| 哈希表 + 递归(DFS) | O(n) | O(n) | 代码简洁,利用递归栈 |
| 原地复制 + 拆分 | O(n) | O(1) | 面试首选,满足进阶要求 |
三、解法一:哈希表法(两次遍历)
3.1 思路
-
第一次遍历:创建所有新节点,并用一个哈希表
Map<Node, Node>存储原节点到新节点的映射。 -
第二次遍历:根据原节点的
next和random指针,通过哈希表找到对应的新节点,设置新节点的next和random。
3.2 代码实现
java
class Solution {
public Node copyRandomList(Node head) {
if (head == null) return null;
// 1. 建立原节点 -> 新节点的映射
Map<Node, Node> map = new HashMap<>();
Node p = head;
while (p != null) {
map.put(p, new Node(p.val));
p = p.next;
}
// 2. 设置新节点的 next 和 random
p = head;
while (p != null) {
Node newNode = map.get(p);
newNode.next = map.get(p.next);
newNode.random = map.get(p.random);
p = p.next;
}
return map.get(head);
}
}
3.3 复杂度分析
-
时间复杂度:O(n),遍历两次链表。
-
空间复杂度:O(n),哈希表存储 n 个映射。
四、解法二:哈希表 + 递归(DFS)
4.1 思路
使用递归深度优先遍历,同样利用哈希表记录已复制的节点。递归函数返回当前节点 head 的深拷贝节点,在复制前先检查哈希表中是否已存在,若存在则直接返回。
4.2 代码实现
java
class Solution {
private Map<Node, Node> map = new HashMap<>();
public Node copyRandomList(Node head) {
if (head == null) return null;
// 如果已经复制过这个节点,直接返回
if (map.containsKey(head)) {
return map.get(head);
}
// 创建新节点,并加入映射
Node newNode = new Node(head.val);
map.put(head, newNode);
// 递归复制 next 和 random
newNode.next = copyRandomList(head.next);
newNode.random = copyRandomList(head.random);
return newNode;
}
}
4.3 复杂度分析
-
时间复杂度:O(n),每个节点被访问常数次。
-
空间复杂度:O(n),哈希表 + 递归调用栈(最坏深度为 n)。
五、解法三:原地复制 + 拆分(O(1) 空间)⭐
5.1 核心思想
不使用额外哈希表,而是将复制节点直接插入到原节点之后,形成"交错链表",然后一次遍历设置随机指针,最后拆分两条链表。
算法步骤:
-
克隆节点并插入 :遍历原链表,对每个节点
cur,创建一个新节点copy,使其val = cur.val,然后将copy插入到cur和cur.next之间。-
原:
A -> B -> C -
变成:
A -> A' -> B -> B' -> C -> C'
-
-
设置随机指针 :再次遍历链表,对于每个原节点
cur,其克隆节点cur.next的随机指针应指向cur.random.next(因为cur.random的原节点对应其克隆节点)。- 注意:如果
cur.random == null,则cur.next.random = null。
- 注意:如果
-
拆分链表 :第三次遍历,将交错链表拆分为原链表和新链表。恢复原链表的
next关系,同时提取出新链表的next关系。
5.2 代码实现
java
class Solution {
public Node copyRandomList(Node head) {
if (head == null) return null;
// 1. 克隆节点并插入到原节点之后
Node cur = head;
while (cur != null) {
Node copy = new Node(cur.val);
copy.next = cur.next;
cur.next = copy;
cur = copy.next;
}
// 2. 设置 random 指针
cur = head;
while (cur != null) {
Node copy = cur.next;
if (cur.random != null) {
copy.random = cur.random.next;
}
cur = copy.next;
}
// 3. 拆分链表
Node newHead = head.next;
cur = head;
while (cur != null) {
Node copy = cur.next;
cur.next = copy.next;
if (copy.next != null) {
copy.next = copy.next.next;
}
cur = cur.next;
}
return newHead;
}
}
5.3 图解示例
以 head = [[7,null],[13,0],[11,4],[10,2],[1,0]] 为例(简化示意):
步骤1:克隆插入
text
原链表: 7(rand=null) -> 13(rand->7) -> 11(rand->1) -> 10(rand->11) -> 1(rand->7) -> null
克隆插入后:
7 -> 7' -> 13 -> 13' -> 11 -> 11' -> 10 -> 10' -> 1 -> 1' -> null
步骤2:设置 random
-
7.random = null → 7'.random = null
-
13.random = 7 → 13'.random = 7'(因为 7 后面紧跟着 7')
-
11.random = 1 → 11'.random = 1'
-
10.random = 11 → 10'.random = 11'
-
1.random = 7 → 1'.random = 7'
步骤3:拆分
-
取出所有
'节点:7' -> 13' -> 11' -> 10' -> 1' -> null -
恢复原链表结构:7 -> 13 -> 11 -> 10 -> 1 -> null
5.4 复杂度分析
-
时间复杂度:O(n),遍历链表三次。
-
空间复杂度:O(1),只使用了几个指针变量(不计返回的新链表空间)。
六、解法对比与总结
| 方法 | 时间复杂度 | 空间复杂度 | 是否修改原链表 | 推荐度 |
|---|---|---|---|---|
| 哈希表(两次遍历) | O(n) | O(n) | ❌ | ⭐⭐⭐⭐ |
| 哈希表 + 递归 | O(n) | O(n) | ❌ | ⭐⭐⭐ |
| 原地复制 + 拆分 | O(n) | O(1) | ✅(临时修改,最后恢复) | ⭐⭐⭐⭐⭐ |
6.1 面试建议
-
首选原地复制 + 拆分法:O(n)+O(1) 空间,满足进阶要求,面试官最期待。
-
哈希表法容易理解,可作为第一反应,但需说明可优化空间。
-
注意原地复制法会临时修改原链表,但最终会恢复原状,不影响外部的链表使用。
6.2 常见错误
-
克隆节点插入时顺序错误 :
copy.next = cur.next; cur.next = copy; cur = copy.next;顺序不能颠倒。 -
设置 random 时忘记判空 :
cur.random可能为null,需要处理。 -
拆分链表时丢失新链表的尾部 :需要同时更新
cur.next和copy.next,且注意循环中cur的移动。 -
递归解法中未使用缓存:可能导致重复创建节点,造成无限递归或错误。
七、相关链接
-
官方题解 :随机链表的复制官方题解