138 随机链表的复制:用 HashMap 做深拷贝的标准解法
题目给的是一种特殊链表:每个节点除了 next,还有一个 random 指针,random 可能指向链表里任意一个节点,也可能为 null。
目标是做深拷贝:
- 新链表必须由全新节点组成
next和random的指向关系要和原链表完全一致- 复制链表的指针不能指向原链表任何节点
这句话是关键:不能指向原链表任何节点。这意味着不能"偷懒复用引用",必须把指针结构完整映射到新节点上。
java
class Solution {
public Node copyRandomList(Node head) {
HashMap<Node,Node> map = new HashMap<>();
Node cur = head;
while(cur != null){
//将新老节点全部弄进hashmap
Node node = new Node(cur.val);
map.put(cur,node);
cur = cur.next;
}
cur = head;
while(cur != null){
//逐一将next和random指针赋予新复制的节点
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
}
一、为什么复制 random 会让问题变复杂?
如果只有 next,复制很简单:新建节点串起来就行。
但 random 的存在会制造一个难点:
在我复制到某个节点时,它的
random可能指向一个"还没被复制出来"的节点。
比如:当前复制到第 2 个节点,但它 random 指向第 10 个节点。
如果我只用一趟遍历边创建边连指针,很可能遇到"目标节点还不存在"的问题。
所以我需要一个"中间层",用来回答这个问题:
原链表中的某个节点 X,对应的新节点 x 是哪个?
这就是 HashMap 登场的理由。
二、核心思路:建立"原节点 → 新节点"的映射
这份解法用的是:
java
HashMap<Node, Node> map = new HashMap<>();
它的语义非常直观:
- key:原链表里的某个节点
cur - value:它对应复制出来的新节点
node
一旦这张映射表建立起来,后面无论是 next 还是 random,都可以通过 map.get(原节点) 找到对应的新节点。
三、两趟遍历:先"造节点",再"连指针"
1)第一趟遍历:只负责创建新节点并放进 map
java
Node cur = head;
while(cur != null){
Node node = new Node(cur.val);
map.put(cur, node);
cur = cur.next;
}
这一趟做的事可以概括为:
把原链表里每一个节点都复制一个"值相同的新节点",但暂时不处理 next/random。
为什么第一趟不处理指针?
因为只有先保证"所有新节点都存在",第二趟给指针赋值时才不会遇到"目标不存在"的麻烦。
这一趟结束后,map 里已经有了完整的"节点对应关系"。
2)第二趟遍历:根据原链表指针关系,给新节点补上 next/random
java
cur = head;
while(cur != null){
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
这里每一行都非常有"翻译器"的味道:
-
原链表里:
cur.next指向谁新链表里:
map.get(cur).next就指向map.get(cur.next) -
原链表里:
cur.random指向谁新链表里:
map.get(cur).random就指向map.get(cur.random)
这两行的关键点在于:右侧的 map.get(...) 会把"原节点引用"转换成"新节点引用"。
那如果 cur.next == null 呢?
map.get(null) 在 Java 里会返回 null。
所以:
java
map.get(cur).next = map.get(null); // 结果就是 null
这正好符合链表尾部 next=null 的语义。
同理,如果 cur.random == null,赋值结果也自然是 null。
所以这份写法非常优雅:不用写额外的 if (cur.next != null) 判断。
四、最后返回新链表头:为什么是 map.get(head)?
java
return map.get(head);
因为 map 保存了"原节点 → 新节点"的映射:
- 原头节点是
head - 新头节点就是
map.get(head)
如果 head == null,map.get(null) 也是 null,结果仍然正确。
五、这份解法为什么保证是"深拷贝"?
深拷贝的核心要求是:新链表里每个节点必须是新对象,且所有指针都指向新对象。
这份解法的保证来自两点:
- 第一趟遍历里
new Node(cur.val)创建的是全新节点对象 - 第二趟遍历里,
next/random都通过map.get(...)指向"新节点",不会指回原链表节点
因此不会出现"新链表 random 指向旧链表节点"的情况。
六、复杂度分析
设链表长度为 n。
- 时间复杂度:两趟遍历
O(n) + O(n) = O(n) - 空间复杂度:HashMap 存 n 个映射
O(n)
这也是这题的经典解法之一:用空间换清晰和可靠。
七、容易踩的坑(这题最常见的翻车点)
-
只复制 next,不复制 random
看似链表复制完成了,但题目重点就是 random。
-
random 指向旧节点
如果写成
newNode.random = oldNode.random,那就是浅拷贝,直接违反题意。 -
企图一趟遍历完成所有事
如果没有额外结构,很容易遇到"random 指向的节点还没创建"的问题。
八、补充:这题还有 O(1) 额外空间的解法
除了 HashMap 方案,还有一种"节点交错插入 + 拆分链表"的做法,可以做到额外空间 O(1)。
但实现细节更容易写错,也不如 HashMap 方案直观。很多情况下我更愿意先把 HashMap 版本写稳,再考虑优化空间。
总结
这题的本质是"复制一个带任意指针的图结构",HashMap 解法把它拆成了两个稳定步骤:
- 先建立原节点 → 新节点的映射(保证所有目标都存在)
- 再按原结构把
next/random翻译到新节点上(保证指针关系完全一致)
写完这题之后,很多"复杂指针复制"的问题都会变得亲切:只要能建映射,就能把结构完整复刻出来。