LeetCode 138 随机链表的复制|两种解法详解(哈希表 + 原地 O (1) 空间)

前言

链表拷贝是面试高频考点,普通单链表深拷贝只需要处理 next 指针,但本题多了一个随机指针 random,它可以指向链表任意节点甚至 null,直接遍历很难同步复制随机引用,很容易写出浅拷贝 bug。

本文会讲解两种主流解法:

  1. 哈希映射法:逻辑直观、上手简单,适合面试快速写出;
  2. 原地拼接拆分法:空间复杂度优化至 O (1),进阶最优解。

一、题目分析

题目描述

给你一个长度为 n 的链表,每个节点包含三个属性:

  • val:节点值
  • next:指向下一个节点
  • random:随机指针,可指向链表任意节点 / 空

要求完成深拷贝

  1. 新链表全部为全新节点,不能复用原链表任何节点;
  2. 新链表的 nextrandom 指针逻辑和原链表完全一致;
  3. 仅传入原链表头节点 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 代表不指向任何节点。

难点

普通遍历只能复制 nextrandom 指向的节点在遍历过程中不一定已经创建,无法直接赋值,这是本题核心卡点。

二、解法一:哈希映射(空间 O (n),推荐入门)

核心思路

Map 建立原节点 → 拷贝节点的一一映射,分两次遍历完成拷贝:

  1. 第一次遍历:遍历原链表,创建每一个节点的拷贝,存入哈希表;
  2. 第二次遍历 :通过哈希映射,给拷贝节点绑定 nextrandom 指针;
  3. 哈希表可以通过原节点直接拿到对应的新节点,完美解决随机指针找不到目标的问题。

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);
};

分步拆解

  1. 边界判断 :原链表为空直接返回 null

  2. 构建映射:只创建节点,不处理指针,保证所有拷贝节点提前存在;

  3. 绑定指针

    • copy.next = map.get(cur.next):原节点下一个节点的拷贝;
    • copy.random = map.get(cur.random):原随机指向节点的拷贝;
  4. 返回结果:哈希表中头节点对应的拷贝节点就是新链表起点。

复杂度分析

  • 时间复杂度:O (n),两次完整遍历链表,n 为链表节点数量;
  • 空间复杂度:O (n),哈希表存储 n 组节点映射,额外占用内存。

优缺点

✅ 优点:逻辑直白、边界少、调试简单,面试首选;❌ 缺点:需要额外哈希表存储映射,空间开销较大。

三、解法二:原地拼接拆分(空间 O (1),进阶最优)

核心思路

不使用哈希表,直接在原链表每个节点后插入自身拷贝,形成「原节点→拷贝节点」交替链表,分三步处理:

  1. 插入拷贝节点 :链表变为 A → A' → B → B' → C → C' → null
  2. 处理 random 指针 :原节点 cur 的拷贝节点 cur.next,它的 random = cur.random.next
  3. 拆分两条链表:将原链表、拷贝链表拆分为两条独立链表,恢复原链表结构,提取拷贝链表。

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.randomnull,拷贝节点的 random 直接赋值 null

3. 拆分两条链表

  • cur 遍历原节点,copyNode 是它身后的拷贝节点;
  • cur.next = copyNode.next:跳过拷贝节点,恢复原链表;
  • copyNode.next = cur?.next:拷贝节点指向下一个拷贝节点;
  • 提前用 copyHead 保存第一个拷贝节点,作为最终返回值。

复杂度分析

  • 时间复杂度:O (n),三次完整遍历链表;
  • 空间复杂度:O (1),仅使用常数临时变量,无额外存储,原地操作。

优缺点

✅ 优点:空间最优,不占用额外内存;❌ 缺点:步骤繁琐,拆分时边界容易出错,逻辑理解成本更高。

四、两种解法对比总结

表格

解法 空间复杂度 实现难度 适用场景
哈希映射法 O(n) 面试快速答题、新手练习
原地拼接拆分 O(1) 追求极致空间、进阶优化场景

面试建议

  1. 基础作答:优先写哈希映射法,代码短、逻辑清晰,几乎不会出现边界错误;
  2. 加分优化:面试官追问「能否不开额外空间」时,再讲解原地拼接方案,体现算法优化思维。

五、踩坑提醒

  1. 浅拷贝误区 :不能直接 newHead = head,只是引用赋值,修改会污染原链表;
  2. random 空值处理 :两种解法都要判断 random 是否为 null,否则调用 .next 会报语法错误;
  3. 原地拆分边界 :拆分最后一组节点时,cur 可能为 null,拷贝节点 next 必须置空,防止循环引用;
  4. 不要破坏原链表:原地法最后会恢复原链表结构,题目未说明不能修改原链表,但业务场景中建议保留原数据。

结语

随机链表复制的核心难点是随机指针无法同步遍历匹配,哈希表用空间换时间,原地法用链表结构特性节省空间。两种思路覆盖了基础和进阶两种考察方向,吃透这道题能彻底掌握带随机指针链表的深拷贝逻辑。

相关推荐
专注于ai算法的踩坑小达人1 小时前
时序预测主流模型综述
算法·lstm
秋天的一阵风2 小时前
🔥 ECMAScript 2026 来了!使用这些新特性,JS 代码直接少一半
前端·javascript·ecmascript 6
前端进阶之旅2 小时前
Next.js 16.3 新特性全解析 AI 驱动开发与 Instant Navigations 实战指南
开发语言·javascript·人工智能
Cobyte2 小时前
编译解析器的基础——状态模式与状态机的原理
前端·javascript·vue.js
HZZD_HZZD2 小时前
碳核查场景下能源计量数据精准采集与碳排放核算算法全链路实战:从智能电表协议适配到排放因子动态计算
算法·能源
饼干哥哥2 小时前
10.5% 深层转化、90%+ 新用户,ChatGPT Ads 值得吗?
算法·正则表达式·编程语言
Irissgwe2 小时前
算法之二分查找
数据结构·c++·算法·二分查找
亿牛云爬虫专家3 小时前
代理IP质量评估:如何建立一套代理IP的多维度评分与淘汰算法?
tcp/ip·算法·爬虫代理·延迟·免费代理ip·连通率·时效
zzz_23683 小时前
【Java实习面试算法冲刺】动态规划
java·算法·面试