LeetCode 138.随机链表的复制 Java
随机链表复制问题详解:哈希映射与分步构建
题目描述
给你一个长度为 n 的链表,每个节点除了包含一个 next 指针指向下一个节点外,还包含一个额外的随机指针 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]]
解释:原链表有5个节点,random指针按索引指向对应节点(null表示指向空)
示例 2:
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
解释:节点0的random指向节点1,节点1的random指向节点1自身
示例 3:
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
解题思路分析
这道题与普通链表复制最大的不同在于 random 指针的存在。如果只复制 next,一次遍历就够了;但 random 可能指向后面尚未创建的节点,导致我们无法在第一次遍历时直接确定它的指向。
我的解题思路如下:
1. 分阶段处理
将复杂问题分解为两个阶段:
- 第一阶段 :只关注
next指针,创建所有新节点,并建立"原节点 → 新节点"的映射关系,同时记录每个新节点对应的"原节点的 random 指向"。 - 第二阶段 :利用第一阶段建立的映射,为每个新节点设置正确的
random指针。
2. 哈希表映射
使用两个哈希表来维护映射关系:
map1:新节点 → 原节点的 random 指向(临时记录)map2:原节点 → 新节点(实现快速查找)
这样在第二阶段,对于任意新节点,我们可以通过 map1 拿到它应该指向的"原节点",再通过 map2 将这个"原节点"转换为对应的"新节点",从而完成 random 指针的赋值。
详细解法步骤
第一步:创建所有新节点并建立映射
遍历原链表,为每个节点创建一个新节点,同时:
- 连接
next指针,形成新链表 - 记录
map1:新节点 → 原节点的 random 指向 - 记录
map2:原节点 → 新节点
java
// 1. 初始化映射与指针
HashMap<Node, Node> map1 = new HashMap<>(); // 新节点 -> 原节点的random指向
HashMap<Node, Node> map2 = new HashMap<>(); // 原节点 -> 新节点
Node current = head; // 原链表遍历指针
Node dummy = new Node(0); // 虚拟头节点
Node newCurrent = dummy; // 新链表构建指针
// 2. 第一次遍历:构建新链表 + 建立映射
while (current != null) {
Node newNode = new Node(current.val);
newCurrent.next = newNode; // 连接next
map1.put(newNode, current.random); // 记录random的"目标原节点"
map2.put(current, newNode); // 记录原节点到新节点的映射
newCurrent = newNode;
current = current.next;
}
第二步:设置所有新节点的 random 指针
遍历新链表,通过两个映射配合,为每个新节点设置 random:
java
// 3. 获取新链表头节点
Node newHead = map2.get(head);
current = newHead;
// 4. 第二次遍历:设置random指针
while (current != null) {
Node originalRandom = map1.get(current); // 原节点的random指向
if (originalRandom != null) {
current.random = map2.get(originalRandom); // 转换为新链表中的节点
} else {
current.random = null;
}
current = current.next;
}
// 5. 返回新链表头节点
return newHead;
关键判断条件解析:
map1.get(current)拿到的是当前新节点对应的"原节点的 random 指向"(可能为null)map2.get(originalRandom)将这个"原节点"转换为新链表中的对应节点- 如果
originalRandom为null,则random也直接设为null
完整代码实现
java
import java.util.HashMap;
class Solution {
/**
* 思路:
* 1. 第一次遍历:创建所有新节点,连接next指针,并用两个哈希表记录映射关系
* - map1: 新节点 -> 原节点的random指向
* - map2: 原节点 -> 新节点
* 2. 第二次遍历:利用map1和map2为新节点的random指针赋值
*
* @param head 原链表的头节点
* @return 深拷贝后的新链表头节点
*/
public Node copyRandomList(Node head) {
public Node copyRandomList(Node head) {
Node current = head; //原链表
Node dummy = new Node(0); //虚拟头节点
Node newCurrent = dummy; //用于连接复制链表
//第一遍先把所有节点对应的新节点创建出来,并用hash表存储node 和 random,先不管新节点的random
HashMap<Node, Node> map1 = new HashMap<>(); //保存 新节点 --> 新节点的随机节点
HashMap<Node, Node> map2 = new HashMap<>(); //保存 当前旧节点 --> 新节点
while(current !=null ){
//创建新节点
Node newNode = new Node(current.val);
newCurrent.next = newNode;
//映射 复制链表中Node 与 对应random 的关系
map1.put(newNode,current.random);
//映射,原链表中Node 与 复制表中Node的关系
map2.put(current,newNode);
//移动指针
newCurrent = newNode;
current = current.next;
}
//遍历设置复制链表的每个Node的random
Node newHead = map2.get(head);
current = newHead;
while(current != null){
current.random = map2.get(map1.get(current));
current = current.next;
}
return newHead;
}
}
}
算法复杂度分析
-
时间复杂度 :O(n)
其中
n是链表节点数。我们只遍历了链表两次,每次都是 O(n),哈希表的插入和查找操作均为 O(1),因此总时间复杂度为 O(n)。 -
空间复杂度 :O(n)
主要来自两个哈希表,每个表最多存储
n个映射,以及新链表本身占用的空间。若不计新链表所占空间(题目要求必须新建节点),则额外空间为 O(n)。
关键点总结
- 分阶段处理:将复杂问题拆分为"构建结构"和"处理随机指针"两个阶段,降低思考难度。
- 哈希表的作用 :通过
map2建立原节点到新节点的快速查找,解决了random指向"未知节点"的问题。 - 虚拟头节点的使用 :简化了新链表
next指针的构建,避免了单独处理第一个节点的特殊逻辑。 - 空指针处理 :注意
random可能为null,代码中需要显式判断,避免空指针异常。
扩展思考
-
如果要求空间复杂度为 O(1),该如何实现?
(提示:可以在原链表中穿插新节点,利用原节点的
next临时存储映射,最后再拆分链表) -
如果链表非常长,哈希表可能会占用较多内存,有没有更节省内存的方式?
-
如果
random指针指向的不仅是当前链表的节点,还可能指向其他链表,这道题应该如何改造?
实际应用场景
虽然带有随机指针的链表在实际工程中并不常见,但这种"分阶段构建 + 哈希映射"的思想在以下场景中非常有用:
- 对象深拷贝:当对象中存在循环引用或交叉引用时,需要先创建所有对象,再建立引用关系
- 图结构克隆:图的深拷贝与本题思路高度一致,也需要记录原节点到新节点的映射
- 序列化与反序列化:在将复杂对象结构存储后重新还原时,常用类似的两阶段方法