单链表专题(四)-刷题复盘篇-LeetCode 138 随机链表复制|原地拷贝法突破复杂指针操作

写在前面:从普通链表到复杂指针

在前面的单链表专题中,我依次梳理了:

  • 单链表基础操作:插入、删除、反转
  • 快慢指针经典应用:倒数第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

要求返回该链表的深拷贝

  1. 新链表所有节点均为重新申请的内存,不与原链表共用节点
  2. 节点值、next 指向关系、random 指向关系与原链表完全一致
  3. 不能破坏原链表的原有结构

示例:

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)

整个算法一共遍历三次链表:

  1. 第一次:插入所有拷贝节点
  2. 第二次:修正所有拷贝节点的 random 指针
  3. 第三次:拆分两条链表 三次均为线性遍历,总时间复杂度为 O(N)。

空间复杂度:O(1)

算法全程只额外使用了 curpurtmp 等有限几个指针变量,没有开辟数组、哈希表等额外存储空间。

注:返回的拷贝链表属于题目要求的输出结果,不计入额外空间复杂度。


七、个人踩坑复盘

这道题我写的时候踩了几个非常典型的链表题坑,这里记录一下避免后续再犯:

  1. random 映射关系写错 最开始笔误写成了 cur->next,导致随机指针完全指向错误的节点,调试了很久才发现。核心是要明确:我们找的是「random目标节点的拷贝」,不是「当前节点的拷贝」。
  2. 遍历移动时空指针崩溃 最开始写指针移动的时候,没有加 cur != NULL 的判断,走到链表末尾时,cur 变成 NULL 还继续访问 cur->next,直接触发空指针异常。链表题凡是访问 ->next,一定要先确认当前指针非空。
  3. 拆分链表顺序颠倒 一开始先改了原节点的 next,再去取拷贝节点,结果发现地址已经丢了。链表操作的核心准则:修改指针前,先保存要用到的节点地址

八、本地测试工程

为了方便本地调试验证,我额外整理了一套完整的模块化测试工程:

复制代码
copyRandomList/
├── copyRandomList.h   // 头文件、结构体、函数声明
├── copyRandomList.c   // 核心算法实现(本人手写)
└── test_main.c        // 测试主函数(AI 辅助生成)

测试代码包含了节点创建、随机指针构造、链表打印、内存释放等功能,编译后可以直接运行验证结果。 完整工程文件已同步上传 Gitee 仓库。

Gitee 仓库地址

数据结构/8.14 leetcode138. 随机链表的复制深度拷贝 · Luminous/Code_2026 - 码云 - 开源中国


九、总结

LeetCode 138 随机链表复制,是单链表阶段非常有代表性的综合题。 它没有用到复杂的算法思想,却把节点创建、指针修改、链表插入、链表拆分这些基础操作全部糅合在了一起。相比快慢指针类题目,它更考验对链表结构变化的整体把控能力。

刷完这道题我最大的感受是:链表题真正的难点从来不是代码量,而是每一次指针修改之后,你能不能在脑子里清晰地画出当前的链表结构。指针操作越复杂,越要一步一步拆解开,切忌跳步。

后续单链表专题还会继续更新更多经典面试题,代码都会同步到 Gitee 仓库,欢迎一起交流指正。

相关推荐
萌新小白YXY1 小时前
SAE J1587/SAE J1708 协议笔记
笔记·单片机·嵌入式硬件
逆境不可逃1 小时前
【LeetCode 912】排序数组——随机化快速排序详解
数据结构·算法·排序算法
Coder-magician1 小时前
《代码随想录》刷题打卡day31:动态规划-背包问题part02
算法·动态规划
薛定e的猫咪1 小时前
从因果视角解决多智能体协作:细读 SCIC 算法
算法
IT古董1 小时前
【MES学习笔记系列】MES 推荐书籍与资源
笔记·学习
Forever Nore1 小时前
LeetCode 14 最长公共前缀 - 纵向扫描
linux·服务器·leetcode
c238561 小时前
《算法武林谱:四大排序神功与二分寻宝术全解》
数据结构·算法·排序算法
圣保罗的大教堂2 小时前
leetcode 3090. 每个字符最多出现两次的最长子字符串 简单
leetcode
一米阳光86612 小时前
软考(中级)软件设计师核心笔记(9)算法——时间复杂度与空间复杂度、查找算法、排序算法
笔记·算法·职场发展·软考·软件设计师·中级职称