【码道初阶】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 翻译到新节点上(保证指针关系完全一致)

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

相关推荐
.简.简.单.单.2 小时前
Design Patterns In Modern C++ 中文版翻译 第八章 组合
java·c++·设计模式
yyy(十一月限定版)2 小时前
C语言——堆
c语言·开发语言·算法
喜欢吃燃面2 小时前
算法竞赛中的数据结构:图
开发语言·数据结构·c++·学习·算法
七夜zippoe2 小时前
Spring MVC请求处理流程源码分析与DispatcherServlet核心逻辑
java·spring·mvc·过滤器·拦截器
笙枫2 小时前
Agent 进阶设计:状态管理、中间件与多Agent协作
java·服务器·python·ai·中间件
小李小李快乐不已2 小时前
贪心算法理论基础
c++·算法·leetcode·贪心算法
爱喝热水的呀哈喽2 小时前
子模代数。
算法·编辑器
有趣灵魂2 小时前
Java-根据HTTP链接读取文件转换为base64
java·开发语言·http
qq_430855882 小时前
线代第三章向量第三节:向量间的线性关系二
人工智能·算法·机器学习