【码道初阶】Leetcode138:随机链表的复制:用 HashMap 做深拷贝的标准解法

138 随机链表的复制:用 HashMap 做深拷贝的标准解法

题目给的是一种特殊链表:每个节点除了 next,还有一个 random 指针,random 可能指向链表里任意一个节点,也可能为 null

目标是做深拷贝

  • 新链表必须由全新节点组成
  • nextrandom 的指向关系要和原链表完全一致
  • 复制链表的指针不能指向原链表任何节点

这句话是关键:不能指向原链表任何节点。这意味着不能"偷懒复用引用",必须把指针结构完整映射到新节点上。


java 复制代码
class Solution {
    public Node copyRandomList(Node head) {
        HashMap<Node,Node> map = new HashMap<>();
        Node cur = head;
        while(cur != null){
            //将新老节点全部弄进hashmap
            Node node = new Node(cur.val);
            map.put(cur,node);
            cur = cur.next;
        }
        cur = head;
        while(cur != null){
            //逐一将next和random指针赋予新复制的节点
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random);
            cur = cur.next;
        }
        return map.get(head);
        
    }
}

一、为什么复制 random 会让问题变复杂?

如果只有 next,复制很简单:新建节点串起来就行。

random 的存在会制造一个难点:

在我复制到某个节点时,它的 random 可能指向一个"还没被复制出来"的节点。

比如:当前复制到第 2 个节点,但它 random 指向第 10 个节点。

如果我只用一趟遍历边创建边连指针,很可能遇到"目标节点还不存在"的问题。

所以我需要一个"中间层",用来回答这个问题:

原链表中的某个节点 X,对应的新节点 x 是哪个?

这就是 HashMap 登场的理由。


二、核心思路:建立"原节点 → 新节点"的映射

这份解法用的是:

java 复制代码
HashMap<Node, Node> map = new HashMap<>();

它的语义非常直观:

  • key:原链表里的某个节点 cur
  • value:它对应复制出来的新节点 node

一旦这张映射表建立起来,后面无论是 next 还是 random,都可以通过 map.get(原节点) 找到对应的新节点。


三、两趟遍历:先"造节点",再"连指针"

1)第一趟遍历:只负责创建新节点并放进 map

java 复制代码
Node cur = head;
while(cur != null){
    Node node = new Node(cur.val);
    map.put(cur, node);
    cur = cur.next;
}

这一趟做的事可以概括为:

把原链表里每一个节点都复制一个"值相同的新节点",但暂时不处理 next/random。

为什么第一趟不处理指针?

因为只有先保证"所有新节点都存在",第二趟给指针赋值时才不会遇到"目标不存在"的麻烦。

这一趟结束后,map 里已经有了完整的"节点对应关系"。


2)第二趟遍历:根据原链表指针关系,给新节点补上 next/random

java 复制代码
cur = head;
while(cur != null){
    map.get(cur).next = map.get(cur.next);
    map.get(cur).random = map.get(cur.random);
    cur = cur.next;
}

这里每一行都非常有"翻译器"的味道:

  • 原链表里:cur.next 指向谁

    新链表里:map.get(cur).next 就指向 map.get(cur.next)

  • 原链表里:cur.random 指向谁

    新链表里:map.get(cur).random 就指向 map.get(cur.random)

这两行的关键点在于:右侧的 map.get(...) 会把"原节点引用"转换成"新节点引用"

那如果 cur.next == null 呢?

map.get(null) 在 Java 里会返回 null

所以:

java 复制代码
map.get(cur).next = map.get(null); // 结果就是 null

这正好符合链表尾部 next=null 的语义。

同理,如果 cur.random == null,赋值结果也自然是 null

所以这份写法非常优雅:不用写额外的 if (cur.next != null) 判断。


四、最后返回新链表头:为什么是 map.get(head)?

java 复制代码
return map.get(head);

因为 map 保存了"原节点 → 新节点"的映射:

  • 原头节点是 head
  • 新头节点就是 map.get(head)

如果 head == nullmap.get(null) 也是 null,结果仍然正确。


五、这份解法为什么保证是"深拷贝"?

深拷贝的核心要求是:新链表里每个节点必须是新对象,且所有指针都指向新对象。

这份解法的保证来自两点:

  1. 第一趟遍历里 new Node(cur.val) 创建的是全新节点对象
  2. 第二趟遍历里,next/random 都通过 map.get(...) 指向"新节点",不会指回原链表节点

因此不会出现"新链表 random 指向旧链表节点"的情况。


六、复杂度分析

设链表长度为 n。

  • 时间复杂度:两趟遍历 O(n) + O(n) = O(n)
  • 空间复杂度:HashMap 存 n 个映射 O(n)

这也是这题的经典解法之一:用空间换清晰和可靠。


七、容易踩的坑(这题最常见的翻车点)

  1. 只复制 next,不复制 random

    看似链表复制完成了,但题目重点就是 random。

  2. random 指向旧节点

    如果写成 newNode.random = oldNode.random,那就是浅拷贝,直接违反题意。

  3. 企图一趟遍历完成所有事

    如果没有额外结构,很容易遇到"random 指向的节点还没创建"的问题。


八、补充:这题还有 O(1) 额外空间的解法

除了 HashMap 方案,还有一种"节点交错插入 + 拆分链表"的做法,可以做到额外空间 O(1)。

但实现细节更容易写错,也不如 HashMap 方案直观。很多情况下我更愿意先把 HashMap 版本写稳,再考虑优化空间。


总结

这题的本质是"复制一个带任意指针的图结构",HashMap 解法把它拆成了两个稳定步骤:

  1. 先建立原节点 → 新节点的映射(保证所有目标都存在)
  2. 再按原结构把 next/random 翻译到新节点上(保证指针关系完全一致)

写完这题之后,很多"复杂指针复制"的问题都会变得亲切:只要能建映射,就能把结构完整复刻出来。

相关推荐
zsc_1181 分钟前
pvz3解码小游戏求解算法 (二)
算法
我命由我123454 分钟前
Android Jetpack Compose - 组件分类:布局组件、交互组件、文本组件
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
Devin~Y7 分钟前
大厂内容社区面试实录:从 Spring Boot 微服务到 AI RAG 问答(附详细解析)
java·spring boot·redis·elasticsearch·spring cloud·微服务·kafka
Lenyiin8 分钟前
Python数据类型与运算符:深入理解Python世界的基石
java·开发语言·python
fīɡЙtīиɡ ℡8 分钟前
【SpringAi最新版入门(二)】
java·javascript·css·人工智能·css3
hanbr9 分钟前
每日一题day1(Leetcode 76最小覆盖子串)
算法·leetcode
AI科技星10 分钟前
张祥前统一场论中两个电荷定义的统一性解析
开发语言·线性代数·算法·数学建模·平面
代码地平线11 分钟前
C语言实现堆与堆排序详解:从零手写到TopK算法及时间复杂度证明
c语言·开发语言·算法
小江的记录本11 分钟前
【大语言模型】大语言模型——核心概念(预训练、SFT监督微调、RLHF/RLAIF对齐、Token、Embedding、上下文窗口)
java·人工智能·后端·python·算法·语言模型·自然语言处理
炘爚13 分钟前
LeetCode(两两交换链表中的节点)
算法·leetcode·链表