写在前面:从普通链表到复杂指针
在前面的单链表专题中,我依次梳理了:
- 单链表基础操作:插入、删除、反转
- 快慢指针经典应用:倒数第k个节点、回文链表、相交链表
- 环形链表 II:快慢指针判环 + 入环点手写数学推导
这些题目难度循序渐进,但核心始终围绕一件事:如何准确控制节点间的指针关系。
而今天这道 LeetCode 138「随机链表的复制」,在单链表的基础上增加了一个 random 随机指针,让原本的线性链表变成了带随机跳转的复杂结构。
这道题没有引入新的数据结构,本质仍然是链表指针操作,但它同时考察了节点创建、指针连接、节点映射、链表拆分四项能力,是单链表阶段非常经典的综合题,也是面试高频考点。
代码仓库位置: 数据结构/8.14 leetcode138. 随机链表的复制深度拷贝 · Luminous/Code_2026 - 码云 - 开源中国
一、题目描述
给定一个长度为 n 的链表,每个节点由三部分组成:
cpp
struct Node {
int val;
struct Node *next;
struct Node *random;
};
next:指向下一个节点random:可以指向链表中任意节点,也可以指向 NULL
要求返回该链表的深拷贝:
- 新链表所有节点均为重新申请的内存,不与原链表共用节点
- 节点值、next 指向关系、random 指向关系与原链表完全一致
- 不能破坏原链表的原有结构
示例:
bash
原链表:7 -> 13 -> 11 -> 10 -> 1
random 指向:
7 → NULL
13 → 7
11 → 1
10 → 11
1 → 7
输出:结构、值、指针指向完全一致的全新链表
二、为什么这道题有难度?
如果只有 next 指针,复制链表非常简单:遍历一次原链表,逐个新建节点并串联起来即可。
但加入 random 指针后,问题就出现了: 第一次遍历创建节点时,当前节点的 random 指向的节点可能还没被创建,无法直接完成赋值。
最容易想到的解法是哈希表映射法: 遍历两次链表,第一次建立「原节点地址 → 新节点地址」的哈希映射,第二次根据映射关系给 random 指针赋值。 这种方法时间复杂度 O(N),但需要额外 O(N) 的哈希表空间。
而本题最经典的最优解,是原地插入拷贝法: 完全利用链表自身的结构承载映射关系,不需要额外的存储容器,将额外空间复杂度优化到 O(1)。
三、原地拷贝法 三步完整拆解
整个算法可以清晰地拆成三个独立步骤,每一步只做一件事,逻辑非常清晰。
第一步:原节点后插入拷贝节点
遍历原链表,在每一个原节点的紧后方,插入一个值完全相同的拷贝节点。
变化过程:
插入前:A -> B -> C -> NULL
插入后:A -> A' -> B -> B' -> C -> C' -> NULL
(A' 表示 A 的拷贝节点)
这一步的核心目的:用链表的相邻关系,天然建立「原节点 → 拷贝节点」的映射。 即:任意原节点的拷贝节点 = 原节点->next。
对应插入函数代码:
cpp
// 在当前节点后插入拷贝节点,返回下一个原节点
Node* Push (Node* node)
{
Node* newnode= malloc(sizeof(Node));
newnode->next=node->next;
node->next=newnode;
newnode->val=node->val;
return newnode->next;
}
第二步:修正拷贝节点的 random 指针
这是整个算法最巧妙的一步。
原节点 cur 的 random 指向 cur->random,那么对应的拷贝节点 pur,它的 random 就应该指向「原随机节点的拷贝节点」。
结合第一步建立的相邻映射关系: 原随机节点的拷贝节点 = cur->random->next
因此核心赋值语句为:
pur->random = cur->random->next;
如果原节点的 random 为 NULL,那么拷贝节点的 random 也直接设为 NULL 即可。
第三步:拆分两条独立链表
前两步完成后,原节点和拷贝节点交替串联在一起。 最后一步需要把这条混合链表,拆回两条独立的链表:
- 恢复原链表原本的结构
- 提取所有拷贝节点,组成新链表作为返回结果
核心逻辑:跳过一个节点连接
- 原节点的 next,指向下一个原节点(即拷贝节点的 next)
- 拷贝节点的 next,指向下一个拷贝节点(即下一个原节点的 next)
拆分完成后,原链表完好如初,新链表是完全独立的深拷贝结果。
四、本人力扣 AC 原版代码
cpp
/**
* Definition for a Node.
* struct Node {
* int val;
* struct Node *next;
* struct Node *random;
* };
*/
typedef struct Node Node;
//插入
Node* Push (Node* node)
{
Node* newnode= malloc(sizeof(Node));
newnode->next=node->next;
node->next=newnode;
newnode->val=node->val;
return newnode->next;
}
struct Node* copyRandomList(struct Node* head) {
// 边界:空链表直接返回
if (head == NULL)
return NULL;
Node*cur = head;
while(cur)
{
cur = Push (cur);
}
cur = head;
Node*pur = cur->next;
while(cur)
{
if(cur->random==NULL)
{
pur->random=NULL;
}
else
{
pur->random=cur->random->next;
}
// 向后移动
cur = pur->next;
if (cur != NULL)
pur = cur->next;
}
Node* newHead = head->next;
cur = head;
Node* tmp=NULL;
while (cur != NULL)
{
tmp = cur->next;
cur->next = tmp->next;
cur = cur->next;
if (tmp->next != NULL)
tmp->next = tmp->next->next;
}
return newHead;
}
五、关键代码细节分析
1. Push 插入函数
这是标准的链表中间插入操作,核心两行不能写反:
cpp
newnode->next = node->next; // 先接后面的链
node->next = newnode; // 再接前面的链
写反会直接导致后面的链表丢失。 函数返回 newnode->next,也就是下一个原节点,方便外层循环持续向后遍历。
2. random 指针赋值
这是最容易写错的一行。
错误写法: pur->random = cur->next;
正确写法: pur->random = cur->random->next;
很多人会下意识写成当前节点的拷贝,但实际上我们要找的是random指向的那个节点的拷贝,必须多跳一层。
3. 链表拆分的顺序
拆分时一定要先暂存拷贝节点,再修改指针:
cpp
tmp = cur->next; // 先保存拷贝节点地址
cur->next = tmp->next; // 再修改原节点的next
如果先修改 cur->next,就会丢失拷贝节点的地址,导致后续断链。 同时末尾节点必须做空判断,否则访问 NULL->next 会直接触发空指针异常。
六、复杂度分析
时间复杂度:O(N)
整个算法一共遍历三次链表:
- 第一次:插入所有拷贝节点
- 第二次:修正所有拷贝节点的 random 指针
- 第三次:拆分两条链表 三次均为线性遍历,总时间复杂度为 O(N)。
空间复杂度:O(1)
算法全程只额外使用了 cur、pur、tmp 等有限几个指针变量,没有开辟数组、哈希表等额外存储空间。
注:返回的拷贝链表属于题目要求的输出结果,不计入额外空间复杂度。
七、个人踩坑复盘
这道题我写的时候踩了几个非常典型的链表题坑,这里记录一下避免后续再犯:
- random 映射关系写错 最开始笔误写成了
cur->next,导致随机指针完全指向错误的节点,调试了很久才发现。核心是要明确:我们找的是「random目标节点的拷贝」,不是「当前节点的拷贝」。 - 遍历移动时空指针崩溃 最开始写指针移动的时候,没有加
cur != NULL的判断,走到链表末尾时,cur变成 NULL 还继续访问cur->next,直接触发空指针异常。链表题凡是访问->next,一定要先确认当前指针非空。 - 拆分链表顺序颠倒 一开始先改了原节点的 next,再去取拷贝节点,结果发现地址已经丢了。链表操作的核心准则:修改指针前,先保存要用到的节点地址。
八、本地测试工程
为了方便本地调试验证,我额外整理了一套完整的模块化测试工程:
copyRandomList/
├── copyRandomList.h // 头文件、结构体、函数声明
├── copyRandomList.c // 核心算法实现(本人手写)
└── test_main.c // 测试主函数(AI 辅助生成)
测试代码包含了节点创建、随机指针构造、链表打印、内存释放等功能,编译后可以直接运行验证结果。 完整工程文件已同步上传 Gitee 仓库。
Gitee 仓库地址
数据结构/8.14 leetcode138. 随机链表的复制深度拷贝 · Luminous/Code_2026 - 码云 - 开源中国
九、总结
LeetCode 138 随机链表复制,是单链表阶段非常有代表性的综合题。 它没有用到复杂的算法思想,却把节点创建、指针修改、链表插入、链表拆分这些基础操作全部糅合在了一起。相比快慢指针类题目,它更考验对链表结构变化的整体把控能力。
刷完这道题我最大的感受是:链表题真正的难点从来不是代码量,而是每一次指针修改之后,你能不能在脑子里清晰地画出当前的链表结构。指针操作越复杂,越要一步一步拆解开,切忌跳步。
后续单链表专题还会继续更新更多经典面试题,代码都会同步到 Gitee 仓库,欢迎一起交流指正。