题目
给你一个长度为 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 作为传入参数。
数据范围
0 <= n <= 1000
-104 <= Node.val <= 104
Node.random 为 null 或指向链表中的节点。
测试用例
示例1

java
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例2

java
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例3

java
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
代码(官解1 哈希回溯,时空复杂度On)
java
class Solution {
Map<Node, Node> cachedNode = new HashMap<Node, Node>();
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
if (!cachedNode.containsKey(head)) {
Node headNew = new Node(head.val);
cachedNode.put(head, headNew);
headNew.next = copyRandomList(head.next);
headNew.random = copyRandomList(head.random);
}
return cachedNode.get(head);
}
}
题解2(官解2,迭代加节点拆分,空间On,时间O1)
java
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
// 0. 特殊情况处理:如果是空链表,直接返回 null
if(head == null)
return head;
// ------------------------------------------------------------
// 第一步:复制每个节点,并将新节点插入到原节点后面
// ------------------------------------------------------------
// 变换前:A -> B -> C
// 变换后:A -> A' -> B -> B' -> C -> C'
// 注意循环步长:node = node.next.next,因为中间插了一个新节点,要跳两步
for(Node node = head; node != null; node = node.next.next){
Node tnode = new Node(node.val); // 创建新节点(拷贝值)
tnode.next = node.next; // 新节点指向原节点的下一个节点
node.next = tnode; // 原节点指向新节点
}
// ------------------------------------------------------------
// 第二步:构建新节点的 random 指针
// ------------------------------------------------------------
// 此时链表结构是:原 -> 新 -> 原 -> 新
// 核心逻辑:因为 A' 在 A 后面,B' 在 B 后面。
// 如果 A.random 指向 B,那么 A'.random 应该指向 B' (即 B.next)
for(Node node = head; node != null; node = node.next.next){
Node tnode = node.next; // tnode 就是上面的 A' (新节点)
if(node.random != null){
// node.random 是原目标节点,node.random.next 就是该目标的副本
tnode.random = node.random.next;
} else {
tnode.random = null;
}
}
// ------------------------------------------------------------
// 第三步:拆分链表(还原原链表,提取新链表)
// ------------------------------------------------------------
// 目标:
// 1. 恢复原链表:A -> B -> C
// 2. 提取新链表:A' -> B' -> C'
Node res = head.next; // 保存新链表的头节点(即 A'),用于最后返回
// 这里循环的更新条件是 node = node.next
// 因为在循环体内,我们已经把 node.next 修改回了原链表的下一个节点
for(Node node = head; node != null; node = node.next){
Node tnode = node.next; // 当前的新节点
// 1. 恢复原链表的 next 指针
// 让 A 直接指向 B (跳过 A')
node.next = node.next.next;
// 2. 链接新链表的 next 指针
// 让 A' 指向 B'。如果后面没有节点了,就指向 null
// (这就是你刚才问的判空逻辑,防止空指针异常)
tnode.next = (tnode.next != null) ? tnode.next.next : null;
}
// 返回新链表的头节点
return res;
}
}
思路
这道题反而比昨天的困难标注的题更像一个困难题,主要在之前的链表题中,都只是普通的迭代或者递归,很少设计其他知识,这道题中涉及到了回溯与节点拆分。回溯代码更简单,节点拆分思路更清晰空间更小,建议大家都掌握。
递归虽然代码简单,但是对没有递归思维的人来说很麻烦这里简单拆解一下:
java
递归逻辑拆解
函数 copyRandomList(head) 的定义是:给我一个原节点 head,我返回拷贝好的新节点。
代码执行流程如下:
终止条件(Base Case):
如果 head 是 null,说明走到头了,直接返回 null。
查缓存(Memoization):
问:head 这个节点之前来过吗?(在 cachedNode 里吗?)
答:如果来过,说明它对应的副本已经创建好了。直接返回缓存里的副本,千万不要再递归了(否则就死循环了)。
创建与递归(Recursive Step):
如果没来过,说明这是第一次遇到 head。
Create: 立刻创建一个新节点 headNew,值等于 head.val。
Put: (关键) 在继续递归之前,先把这个新老节点的映射关系存入 cachedNode。
为什么要先存? 因为后续递归 head.next 或 head.random 时,它们可能会指回到 head。如果不先存进去,后面递归回来找 head 时发现没有,又会创建一个新的,导致死循环或逻辑错误。
Connect:
headNew.next = copyRandomList(head.next); (去吧,把后面那一串复制好连上)
headNew.random = copyRandomList(head.random); (去吧,把 random 指向的那一串复制好连上)
返回结果:
返回当前创建好的 headNew。