今天我们来看一道非常经典的链表题:随机链表的复制。第一次看到这道题时,有点蒙圈。但经过一番挣扎、画图和思考,最终领悟了其中“O(1) 空间复杂度”的精妙解法。
这篇博客将完全以一个初学者的视角展开,记录我每一步的困惑、尝试和突破,希望能给你带来启发。
给你一个长度为 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 作为传入参数。

当我看到题目要求:给你一个链表,除了常规的 next 指针外,每个节点还有一个 random 指针,它可能指向链表中的任意节点或 NULL。要求你深拷贝这个链表。
random 指针next 指针连起来就行了。random 指针。它的指向毫无规律,就无法用常规思路去写。为什么麻烦?
假设原链表是
。我们创建了拷贝链表
。
时,我怎么知道
的 random 应该指向谁?
,那么
应该指向
。
时,
可能还没创建!我怎么指向一个不存在的东西?
故浅拷贝不行! 新节点必须完全独立。不能简单地让
,因为
指向的是原链表中的节点,而不是新链表中的拷贝节点。
第一反应:画图!
画图就可以清楚地认识到:我们需要一个映射关系,来告诉我在原链表中 A 对应的拷贝是 A’。
既然需要映射关系,我立刻想到了最直接、最笨的方法:哈希表(Hash Map)。
,就创建一个拷贝节点
。
。
和
)
。
。
的 next:
的 random:
这个方法非常有效且容易理解。时间复杂度
(两次遍历),空间复杂度
(哈希表存储
个节点)。 它能解决问题,但还能继续优化吗?能不能不用额外空间,达到
空间复杂度?
我盯着链表结构图发呆,心想:既然
空间是浪费在哈希表上,那有没有一种天然的映射,可以替代哈希表?
为什么不把拷贝节点直接插到原节点的旁边呢?
,它的拷贝节点
永远是
。
有了这个天然的
查找映射,我就可以进入最难的部分了!
现在,我的链表是混合的,我需要设置拷贝节点 (
) 的
指针。
假设原节点
的
指向
。即
。 那么,它的拷贝节点
的
应该指向
。即
。
?
找到它
指向的原节点
:
就在
的隔壁:
画图辅助写代码:

用代码来表达就是:
// 当前节点是 cur (原节点), 它的拷贝节点是 copy (cur->next)
struct Node* copy = cur->next;
if (cur->random != NULL) {
// 假设 cur->random 指向 C,那么 C 的拷贝 C' 就在 C 的 next
copy->random = cur->random->next;
} else {
// 边界情况:如果原节点的 random 是 NULL,那么拷贝的 random 也是 NULL
copy->random = NULL;
}至此,
指针的问题在
的时间复杂度下解决了!
现在,我有一个完美复制了
和
的混合链表:
。
我最后的任务是:
我们可以在一次遍历中完成这两件事。我们用两个指针
和
来分别维护原链表和新链表.
负责恢复原链表的
:cur->next = cur->next->next (即
)
负责构建新链表的
:copy->next = copy->next->next (即
)
将这三个阶段整理成一个清晰的三步走流程,并用 C 语言实现,添加详细的注释来解释每一步的思考。
struct Node* copyRandomList(struct Node* head)
{
if (head == NULL)
return NULL; // 边界情况1:空链表,返回 NULL
// --- 步骤 1: 插入拷贝节点 (建立 O(1) 映射) ---
// 我需要一个天然的映射,代替 O(N) 的哈希表。
// 策略:把拷贝节点插在原节点后面,即 A -> A' -> B -> B'
struct Node* cur = head;
while (cur != NULL)
{
// 创建 A' 节点,值和 A 一样
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
copy->val = cur->val;
// A' 的 next 指向 B
copy->next = cur->next;
// A 的 next 指向 A'
cur->next = copy;
// 移动到下一个原节点 B (即 A' 的 next)
cur = copy->next;
}
// --- 步骤 2: 复制 random 指针 (利用 O(1) 映射) ---
// 现在我们有 A -> A' -> B -> B' 的结构
// 核心公式:A'.random = A.random->next (因为 A.random->next 就是 A.random 的拷贝节点)
cur = head;
while (cur != NULL)
{
struct Node* copy = cur->next;
// 边界情况2:如果原节点的 random 是 NULL
if (cur->random != NULL) {
// A.random 假设是 C,那么 C 的拷贝 C' 就是 C.next
copy->random = cur->random->next;
else
copy->random = NULL;
// 移动到下一个原节点 B (即 A' 的 next)
cur = copy->next;
}
// --- 步骤 3: 拆分链表 (恢复原链表结构并构建新链表) ---
// 拆分:A -> A' -> B -> B' 变成 A -> B 和 A' -> B'
struct Node* new_head = head->next; // 新链表的头节点 (即 A')
cur = head;
// 为什么要用 new_head 作为新链表的头?
// 因为 head (A) 永远是原链表的头,它的 next (A') 永远是新链表的头。
while (cur != NULL)
{
struct Node* copy = cur->next; // 拷贝节点 (A')
// 1. 恢复原链表的 next: A -> B
cur->next = copy->next;
// 2. 构建新链表的 next: A' -> B'
if (copy->next != NULL)
// copy->next 此时指向 B。B 的拷贝 B' 就在 B 的 next 位置
copy->next = copy->next->next;
else
// 边界情况3:如果是最后一个拷贝节点 C',它的 next 应该指向 NULL
copy->next = NULL;
// 移动到下一个原节点 B
cur = cur->next;
}
// 返回新链表的头节点
return new_head;
}为了验证算法,手动构造了一个小例子,并画出每一步的链表状态:
测试用例:
。且
,
。
节点 | 初始状态 | 插入后 |
|---|---|---|
A | A → B A \to B A→B | A → A ′ → B A \to A' \to B A→A′→B |
B | B → N U L L B \to NULL B→NULL | B → B ′ → N U L L B \to B' \to NULL B→B′→NULL |
结果 | A → A ′ → B → B ′ → N U L L A \to A' \to B \to B' \to NULL A→A′→B→B′→NULL |
B
结果
当前节点 cur \text{cur} cur | 拷贝节点 copy \text{copy} copy | cur.random \text{cur.random} cur.random | copy.random \text{copy.random} copy.random 设置为 | 验证公式 |
|---|---|---|---|---|
A | A’ | B | B’ ( B.next \text{B.next} B.next) | A.random.next = B.next = B’ \text{A.random.next} = \text{B.next} = \text{B'} A.random.next=B.next=B’ ✓ |
B | B’ | A | A’ ( A.next \text{A.next} A.next) | B.random.next = A.next = A’ \text{B.random.next} = \text{A.next} = \text{A'} B.random.next=A.next=A’ ✓ |
拷贝节点
设置为验证公式AA’BB’ (
)
✓BB’AA’ (
)
✓
当前节点 cur \text{cur} cur | 拷贝节点 copy \text{copy} copy | 恢复 cur.next \text{cur.next} cur.next | 构建 copy.next \text{copy.next} copy.next |
|---|---|---|---|
A | A’ | A.next → B \text{A.next} \to \text{B} A.next→B | A’.next → B’ \text{A'.next} \to \text{B'} A’.next→B’ |
B | B’ | B.next → NULL \text{B.next} \to \text{NULL} B.next→NULL | B’.next → NULL \text{B'.next} \to \text{NULL} B’.next→NULL |
拷贝节点
恢复
构建
AA’
BB’
最终结果:
(结构已恢复)
,且
,
(复制成功)
的时间替代了
空间的哈希表映射。这是从
空间到
空间的关键一步。
cur->random->next 的公式,快速定位 对应的拷贝节点.
指针,再引入
指针。
的解法,就不会去探索这个
空间的精妙技巧了。
希望我的这份“初学者思考路径”能帮助你彻底理解并掌握这道随机链表的复制问题!