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

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

相关推荐
記億揺晃着的那天5 分钟前
Windows 通过 Java 获取可用端口的一个坑:Hyper-V 保留端口导致 UDP 绑定失败
java·windows·udp
组合缺一7 分钟前
SolonCode(编码智能体)支持鸿蒙 PC
java·华为·ai·ai编程·harmonyos·solon·soloncode
小bo波8 分钟前
用匿名内部类优雅地计算方法执行时间
java·设计模式·性能测试·模板方法模式·lambda·代码优化·匿名内部类
折哥的程序人生 · 物流技术专研12 分钟前
Tomcat 严重警告:JDBC 驱动未注销 + 工作线程泄漏 —— 原因、影响与彻底修复(生产级终极指南)
java·运维·数据库·mysql·oracle·tomcat
一个儒雅随和的男子16 分钟前
sentinel底层原理剖析以及实战优化
java·网络·sentinel
洛水水18 分钟前
【力扣100题】76.搜索插入位置
数据结构·算法·leetcode
Techblog of HaoWANG21 分钟前
智巡守卫:多模态巡检智能体算法服务端设计与实现——基于Ollama+Qwen3.5的自动化巡检报告生成系统
运维·人工智能·算法·目标检测·自动化·边缘计算
两年半的个人练习生^_^22 分钟前
JMM 进阶:彻底理解 synchronized 实现原理
java·开发语言
戳代码的新星29 分钟前
论小白如何学会使用Maven
java·maven
wyhwust29 分钟前
maven的安装和配置
java