力扣解题-138. 复制带随机指针的链表
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
- val:一个表示 Node.val 的整数。
- random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。
示例 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⁴
Node.random 为 null 或指向链表中的节点。
Related Topics
哈希表、链表
第一次解答
解题思路
核心方法:哈希表映射法,通过HashMap建立“原节点→新节点”的映射关系,分两步完成深拷贝:第一步创建所有新节点并存储映射,第二步遍历原链表设置新节点的next和random指针,逻辑直观且易于理解,时间复杂度O(n)、空间复杂度O(n)。
核心逻辑拆解
深拷贝带随机指针链表的核心难点是“random指针无法按顺序直接复制”,哈希表的作用是建立原节点与新节点的一一对应关系:
- 边界处理:若原链表头节点
head为null,直接返回null(空链表无需拷贝); - 第一步:创建新节点并建立映射:
- 初始化HashMap,定义
cur指针从head开始遍历原链表; - 遍历过程中,为每个原节点
cur创建值相同的新节点new Node(cur.val); - 将
cur(原节点)作为key、新节点作为value存入HashMap; - 遍历完成后,HashMap中存储了所有原节点与对应新节点的映射;
- 初始化HashMap,定义
- 第二步:设置新节点的next和random指针:
- 重置
cur指针到head,再次遍历原链表; - 对每个原节点
cur,从HashMap中取出对应的新节点newNode; - 设置
newNode.next:等于HashMap中cur.next对应的新节点(cur.next为null时,map.get返回null,符合要求); - 设置
newNode.random:等于HashMap中cur.random对应的新节点(同理,null时返回null);
- 重置
- 返回结果:从HashMap中取出
head对应的新节点,即为拷贝链表的头节点。
具体步骤(以示例1 head=[[7,null],[13,0],[11,4],[10,2],[1,0]]为例)
| 步骤 | 原节点 | 新节点 | HashMap映射 | next/random设置 |
|---|---|---|---|---|
| 1 | 7(null) | 7(null) | 7→7 | - |
| 2 | 13(0) | 13(null) | 13→13 | - |
| 3 | 11(4) | 11(null) | 11→11 | - |
| 4 | 10(2) | 10(null) | 10→10 | - |
| 5 | 1(0) | 1(null) | 1→1 | - |
| 6 | 7(null) | 7(null) | - | newNode.next=13,newNode.random=null |
| 7 | 13(0) | 13(null) | - | newNode.next=11,newNode.random=7 |
| 8 | 11(4) | 11(null) | - | newNode.next=10,newNode.random=1 |
| 9 | 10(2) | 10(null) | - | newNode.next=1,newNode.random=11 |
| 10 | 1(0) | 1(null) | - | newNode.next=null,newNode.random=7 |
| 最终拷贝链表与原链表结构完全一致,且所有指针指向新节点。 |
性能说明
- 时间复杂度:O(n)(两次线性遍历原链表,HashMap的get/put操作均为O(1));
- 空间复杂度:O(n)(HashMap存储n个节点的映射关系);
- 优势:
- 逻辑清晰,将“拷贝节点”和“设置指针”拆分为两步,降低问题复杂度;
- 无需处理复杂的指针插入/拆分逻辑,新手易实现;
- 天然支持random指针指向null或任意节点的场景,无边界遗漏。
public Node copyRandomList(Node head) {
if(head==null){
return null;
}
Map<Node,Node> map=new HashMap<>();
Node cur=head;
while(cur!=null){
map.put(cur,new Node(cur.val));
cur=cur.next;
}
cur=head;
while(cur!=null){
Node newNode=map.get(cur);
newNode.next=map.get(cur.next);
newNode.random=map.get(cur.random);
cur=cur.next;
}
return map.get(head);
}
示例解答
解题思路
解法1:原地拆分法(最优解,O(1)额外空间)
核心方法:原地插入新节点+拆分链表,无需哈希表,通过在原链表每个节点后插入对应的拷贝节点,利用原节点的指针关系设置拷贝节点的random指针,最后拆分原链表和拷贝链表,时间复杂度O(n)、额外空间复杂度O(1)(满足进阶优化要求)。
核心原理铺垫
该方法的核心是“利用原链表的指针位置,直接关联拷贝节点”:
- 第一步:在每个原节点后插入值相同的拷贝节点(如原链表A→B→C变为A→A'→B→B'→C→C');
- 第二步:利用原节点的random指针,设置拷贝节点的random(如A.random=B → A'.random=B');
- 第三步:拆分链表,将A'→B'→C'从原链表中分离,得到拷贝链表。
核心逻辑拆解
- 边界处理:若
head为null,返回null; - 第一步:插入拷贝节点:
- 定义
cur指针指向head,遍历原链表; - 对每个原节点
cur,创建拷贝节点copy = new Node(cur.val); - 将
copy插入到cur和cur.next之间(copy.next = cur.next; cur.next = copy); cur移动到cur.next.next(跳过拷贝节点,继续遍历原节点);
- 定义
- 第二步:设置拷贝节点的random指针:
- 重置
cur到head,再次遍历原链表; - 对每个原节点
cur,其拷贝节点为cur.next; - 若
cur.random != null,则cur.next.random = cur.random.next(原节点random的拷贝节点); - 若
cur.random == null,拷贝节点的random也为null(无需处理);
- 重置
- 第三步:拆分链表:
- 定义
dummy哑节点和copyCur指针指向dummy; - 重置
cur到head,遍历原链表,每次取出拷贝节点cur.next接入dummy链表; - 恢复原链表的结构(
cur.next = cur.next.next),cur继续遍历;
- 定义
- 返回结果:返回
dummy.next(拷贝链表的头节点)。
代码实现
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
// 第一步:在每个原节点后插入拷贝节点
Node cur = head;
while (cur != null) {
Node copy = new Node(cur.val);
copy.next = cur.next;
cur.next = copy;
cur = copy.next; // 跳过拷贝节点,遍历下一个原节点
}
// 第二步:设置拷贝节点的random指针
cur = head;
while (cur != null) {
Node copy = cur.next;
// 原节点random不为空时,拷贝节点的random指向原random的拷贝节点
if (cur.random != null) {
copy.random = cur.random.next;
}
cur = copy.next; // 跳过拷贝节点
}
// 第三步:拆分原链表和拷贝链表
cur = head;
Node dummy = new Node(0);
Node copyCur = dummy;
while (cur != null) {
// 取出拷贝节点
Node copy = cur.next;
// 恢复原链表
cur.next = copy.next;
// 将拷贝节点接入新链表
copyCur.next = copy;
copyCur = copyCur.next;
// 遍历下一个原节点
cur = cur.next;
}
return dummy.next;
}
性能说明
- 时间复杂度:O(n)(三次线性遍历原链表,无嵌套操作);
- 额外空间复杂度:O(1)(仅使用几个指针变量,无哈希表等额外存储);
- 核心优势:
- 无需额外空间,满足进阶优化要求;
- 仅操作指针,内存开销极小,执行效率更高;
- 注意事项:
- 拆分链表时需恢复原链表结构,避免破坏输入数据;
- 处理random指针时需先判断原节点random是否为null,避免空指针异常。
总结
- 哈希表映射法(第一次解答):O(n)时间+O(n)空间,逻辑直观、易于实现,适合新手理解核心思路;
- 原地拆分法(最优解):O(n)时间+O(1)额外空间,无需哈希表,通过指针操作完成拷贝,工程首选;
- 关键技巧:
- 核心难点:random指针无法按顺序复制,需通过“映射”或“原地关联”解决;
- 哈希表法:用空间换时间,降低逻辑复杂度;
- 原地拆分法:用指针操作替代哈希表,优化空间复杂度,需注意链表拆分时的边界处理。