前言
链表拷贝是面试高频考点,普通单链表深拷贝只需要处理 next 指针,但本题多了一个随机指针 random,它可以指向链表任意节点甚至 null,直接遍历很难同步复制随机引用,很容易写出浅拷贝 bug。
本文会讲解两种主流解法:
- 哈希映射法:逻辑直观、上手简单,适合面试快速写出;
- 原地拼接拆分法:空间复杂度优化至 O (1),进阶最优解。
一、题目分析
题目描述
给你一个长度为 n 的链表,每个节点包含三个属性:
val:节点值next:指向下一个节点random:随机指针,可指向链表任意节点 / 空
要求完成深拷贝:
- 新链表全部为全新节点,不能复用原链表任何节点;
- 新链表的
next、random指针逻辑和原链表完全一致; - 仅传入原链表头节点
head作为参数,返回拷贝链表头节点。
输入输出示例
输入:[[7,null],[13,0],[11,4],[10,2],[1,0]]输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
数组格式说明:[val, random_index],random_index 为随机指向节点下标,null 代表不指向任何节点。
难点
普通遍历只能复制 next,random 指向的节点在遍历过程中不一定已经创建,无法直接赋值,这是本题核心卡点。
二、解法一:哈希映射(空间 O (n),推荐入门)
核心思路
用 Map 建立原节点 → 拷贝节点的一一映射,分两次遍历完成拷贝:
- 第一次遍历:遍历原链表,创建每一个节点的拷贝,存入哈希表;
- 第二次遍历 :通过哈希映射,给拷贝节点绑定
next和random指针; - 哈希表可以通过原节点直接拿到对应的新节点,完美解决随机指针找不到目标的问题。
JavaScript 完整代码
javascript
运行
ini
// Definition for a Node.
function Node(val, next, random) {
this.val = val;
this.next = next;
this.random = random;
};
/**
* @param {Node} head
* @return {Node}
*/
var copyRandomList = function(head) {
if (!head) return null;
// key: 原链表节点,value: 对应拷贝节点
const nodeMap = new Map();
let cur = head;
// 第一轮:创建所有拷贝节点,存入Map映射
while (cur) {
nodeMap.set(cur, new Node(cur.val));
cur = cur.next;
}
// 第二轮:赋值next与random指针
cur = head;
while (cur) {
const copyNode = nodeMap.get(cur);
// next指向原节点next对应的拷贝节点,不存在则为null
copyNode.next = nodeMap.get(cur.next) || null;
// random指向原节点random对应的拷贝节点,不存在则为null
copyNode.random = nodeMap.get(cur.random) || null;
cur = cur.next;
}
// 返回原头节点对应的拷贝头节点
return nodeMap.get(head);
};
分步拆解
-
边界判断 :原链表为空直接返回
null; -
构建映射:只创建节点,不处理指针,保证所有拷贝节点提前存在;
-
绑定指针:
copy.next = map.get(cur.next):原节点下一个节点的拷贝;copy.random = map.get(cur.random):原随机指向节点的拷贝;
-
返回结果:哈希表中头节点对应的拷贝节点就是新链表起点。
复杂度分析
- 时间复杂度:O (n),两次完整遍历链表,n 为链表节点数量;
- 空间复杂度:O (n),哈希表存储 n 组节点映射,额外占用内存。
优缺点
✅ 优点:逻辑直白、边界少、调试简单,面试首选;❌ 缺点:需要额外哈希表存储映射,空间开销较大。
三、解法二:原地拼接拆分(空间 O (1),进阶最优)
核心思路
不使用哈希表,直接在原链表每个节点后插入自身拷贝,形成「原节点→拷贝节点」交替链表,分三步处理:
- 插入拷贝节点 :链表变为
A → A' → B → B' → C → C' → null; - 处理 random 指针 :原节点
cur的拷贝节点cur.next,它的random = cur.random.next; - 拆分两条链表:将原链表、拷贝链表拆分为两条独立链表,恢复原链表结构,提取拷贝链表。
JavaScript 完整代码
javascript
运行
ini
function Node(val, next, random) {
this.val = val;
this.next = next;
this.random = random;
};
/**
* @param {Node} head
* @return {Node}
*/
var copyRandomList = function(head) {
if (!head) return null;
let cur = head;
// 第一步:原地插入拷贝节点
while (cur) {
const copy = new Node(cur.val);
copy.next = cur.next;
cur.next = copy;
cur = copy.next;
}
// 第二步:给拷贝节点设置random指针
cur = head;
while (cur) {
const copyNode = cur.next;
// 原节点random不为空时,取它的下一个(对应拷贝节点)
copyNode.random = cur.random ? cur.random.next : null;
cur = copyNode.next;
}
// 第三步:拆分原链表和拷贝链表
cur = head;
const copyHead = head.next; // 保存拷贝链表头节点
while (cur) {
const copyNode = cur.next;
// 恢复原链表next指针
cur.next = copyNode.next;
cur = cur.next;
// 拷贝节点next指向下一个拷贝节点
copyNode.next = cur ? cur.next : null;
}
return copyHead;
};
分步拆解
1. 原地插入拷贝节点
遍历原链表,在每个节点后方插入新拷贝节点:
js
运行
ini
const copy = new Node(cur.val);
copy.next = cur.next; // 新节点接上原节点的下一位
cur.next = copy; // 原节点指向自己的拷贝
cur = copy.next; // 跳到下一个原节点
效果:7 → 7' → 13 → 13' → 11 → 11' ...
2. 填充 random 指针
原节点 cur.random 指向某个原节点,该节点的下一位就是对应的拷贝节点:cur.next.random = cur.random.next如果 cur.random 为 null,拷贝节点的 random 直接赋值 null。
3. 拆分两条链表
cur遍历原节点,copyNode是它身后的拷贝节点;cur.next = copyNode.next:跳过拷贝节点,恢复原链表;copyNode.next = cur?.next:拷贝节点指向下一个拷贝节点;- 提前用
copyHead保存第一个拷贝节点,作为最终返回值。
复杂度分析
- 时间复杂度:O (n),三次完整遍历链表;
- 空间复杂度:O (1),仅使用常数临时变量,无额外存储,原地操作。
优缺点
✅ 优点:空间最优,不占用额外内存;❌ 缺点:步骤繁琐,拆分时边界容易出错,逻辑理解成本更高。
四、两种解法对比总结
表格
| 解法 | 空间复杂度 | 实现难度 | 适用场景 |
|---|---|---|---|
| 哈希映射法 | O(n) | 低 | 面试快速答题、新手练习 |
| 原地拼接拆分 | O(1) | 高 | 追求极致空间、进阶优化场景 |
面试建议
- 基础作答:优先写哈希映射法,代码短、逻辑清晰,几乎不会出现边界错误;
- 加分优化:面试官追问「能否不开额外空间」时,再讲解原地拼接方案,体现算法优化思维。
五、踩坑提醒
- 浅拷贝误区 :不能直接
newHead = head,只是引用赋值,修改会污染原链表; - random 空值处理 :两种解法都要判断
random是否为null,否则调用.next会报语法错误; - 原地拆分边界 :拆分最后一组节点时,
cur可能为null,拷贝节点next必须置空,防止循环引用; - 不要破坏原链表:原地法最后会恢复原链表结构,题目未说明不能修改原链表,但业务场景中建议保留原数据。
结语
随机链表复制的核心难点是随机指针无法同步遍历匹配,哈希表用空间换时间,原地法用链表结构特性节省空间。两种思路覆盖了基础和进阶两种考察方向,吃透这道题能彻底掌握带随机指针链表的深拷贝逻辑。