LeetCode 138.随机链表的复制 Java

LeetCode 138.随机链表的复制 Java

随机链表复制问题详解:哈希映射与分步构建

题目描述

给你一个长度为 n 的链表,每个节点除了包含一个 next 指针指向下一个节点外,还包含一个额外的随机指针 random,该指针可以指向链表中的任何节点或空节点。

要求构造这个链表的深拷贝 。深拷贝应该正好由 n全新节点组成,其中:

  • 每个新节点的值都设为其对应的原节点的值
  • 新节点的 next 指针和 random 指针都应指向复制链表中的新节点
  • 原链表和复制链表中的这些指针能够表示相同的链表状态
  • 复制链表中的指针都不应指向原链表中的节点

示例 1:

复制代码
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
解释:原链表有5个节点,random指针按索引指向对应节点(null表示指向空)

示例 2:

复制代码
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
解释:节点0的random指向节点1,节点1的random指向节点1自身

示例 3:

复制代码
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

解题思路分析

这道题与普通链表复制最大的不同在于 random 指针的存在。如果只复制 next,一次遍历就够了;但 random 可能指向后面尚未创建的节点,导致我们无法在第一次遍历时直接确定它的指向。

我的解题思路如下:

1. 分阶段处理

将复杂问题分解为两个阶段:

  • 第一阶段 :只关注 next 指针,创建所有新节点,并建立"原节点 → 新节点"的映射关系,同时记录每个新节点对应的"原节点的 random 指向"。
  • 第二阶段 :利用第一阶段建立的映射,为每个新节点设置正确的 random 指针。
2. 哈希表映射

使用两个哈希表来维护映射关系:

  • map1:新节点 → 原节点的 random 指向(临时记录)
  • map2:原节点 → 新节点(实现快速查找)

这样在第二阶段,对于任意新节点,我们可以通过 map1 拿到它应该指向的"原节点",再通过 map2 将这个"原节点"转换为对应的"新节点",从而完成 random 指针的赋值。

详细解法步骤

第一步:创建所有新节点并建立映射

遍历原链表,为每个节点创建一个新节点,同时:

  1. 连接 next 指针,形成新链表
  2. 记录 map1:新节点 → 原节点的 random 指向
  3. 记录 map2:原节点 → 新节点
java 复制代码
// 1. 初始化映射与指针
HashMap<Node, Node> map1 = new HashMap<>();  // 新节点 -> 原节点的random指向
HashMap<Node, Node> map2 = new HashMap<>();  // 原节点 -> 新节点

Node current = head;          // 原链表遍历指针
Node dummy = new Node(0);     // 虚拟头节点
Node newCurrent = dummy;      // 新链表构建指针

// 2. 第一次遍历:构建新链表 + 建立映射
while (current != null) {
    Node newNode = new Node(current.val);
    newCurrent.next = newNode;               // 连接next
    map1.put(newNode, current.random);       // 记录random的"目标原节点"
    map2.put(current, newNode);              // 记录原节点到新节点的映射
    newCurrent = newNode;
    current = current.next;
}
第二步:设置所有新节点的 random 指针

遍历新链表,通过两个映射配合,为每个新节点设置 random

java 复制代码
// 3. 获取新链表头节点
Node newHead = map2.get(head);
current = newHead;

// 4. 第二次遍历:设置random指针
while (current != null) {
    Node originalRandom = map1.get(current);          // 原节点的random指向
    if (originalRandom != null) {
        current.random = map2.get(originalRandom);    // 转换为新链表中的节点
    } else {
        current.random = null;
    }
    current = current.next;
}

// 5. 返回新链表头节点
return newHead;

关键判断条件解析:

  • map1.get(current) 拿到的是当前新节点对应的"原节点的 random 指向"(可能为 null
  • map2.get(originalRandom) 将这个"原节点"转换为新链表中的对应节点
  • 如果 originalRandomnull,则 random 也直接设为 null

完整代码实现

java 复制代码
import java.util.HashMap;

class Solution {
    /**
     * 思路:
     * 1. 第一次遍历:创建所有新节点,连接next指针,并用两个哈希表记录映射关系
     *    - map1: 新节点 -> 原节点的random指向
     *    - map2: 原节点 -> 新节点
     * 2. 第二次遍历:利用map1和map2为新节点的random指针赋值
     * 
     * @param head 原链表的头节点
     * @return 深拷贝后的新链表头节点
     */
    public Node copyRandomList(Node head) {
           public Node copyRandomList(Node head) {
        Node current = head; //原链表
        Node dummy = new Node(0);   //虚拟头节点
        Node newCurrent = dummy;    //用于连接复制链表

        //第一遍先把所有节点对应的新节点创建出来,并用hash表存储node 和 random,先不管新节点的random
        HashMap<Node, Node> map1 = new HashMap<>(); //保存 新节点 --> 新节点的随机节点
        HashMap<Node, Node> map2 = new HashMap<>(); //保存 当前旧节点 --> 新节点
        while(current !=null ){
            //创建新节点
            Node newNode = new Node(current.val);
            newCurrent.next = newNode;

            //映射 复制链表中Node 与 对应random 的关系
            map1.put(newNode,current.random);
            //映射,原链表中Node 与 复制表中Node的关系
            map2.put(current,newNode);

            //移动指针
            newCurrent = newNode;
            current = current.next;
        }

        //遍历设置复制链表的每个Node的random
        Node newHead = map2.get(head);
        current = newHead;
        while(current != null){
            current.random = map2.get(map1.get(current));
            current = current.next;
        }

        return newHead;
    }
    }
}

算法复杂度分析

  • 时间复杂度 :O(n)

    其中 n 是链表节点数。我们只遍历了链表两次,每次都是 O(n),哈希表的插入和查找操作均为 O(1),因此总时间复杂度为 O(n)。

  • 空间复杂度 :O(n)

    主要来自两个哈希表,每个表最多存储 n 个映射,以及新链表本身占用的空间。若不计新链表所占空间(题目要求必须新建节点),则额外空间为 O(n)。

关键点总结

  • 分阶段处理:将复杂问题拆分为"构建结构"和"处理随机指针"两个阶段,降低思考难度。
  • 哈希表的作用 :通过 map2 建立原节点到新节点的快速查找,解决了 random 指向"未知节点"的问题。
  • 虚拟头节点的使用 :简化了新链表 next 指针的构建,避免了单独处理第一个节点的特殊逻辑。
  • 空指针处理 :注意 random 可能为 null,代码中需要显式判断,避免空指针异常。

扩展思考

  • 如果要求空间复杂度为 O(1),该如何实现?

    (提示:可以在原链表中穿插新节点,利用原节点的 next 临时存储映射,最后再拆分链表)

  • 如果链表非常长,哈希表可能会占用较多内存,有没有更节省内存的方式?

  • 如果 random 指针指向的不仅是当前链表的节点,还可能指向其他链表,这道题应该如何改造?

实际应用场景

虽然带有随机指针的链表在实际工程中并不常见,但这种"分阶段构建 + 哈希映射"的思想在以下场景中非常有用:

  • 对象深拷贝:当对象中存在循环引用或交叉引用时,需要先创建所有对象,再建立引用关系
  • 图结构克隆:图的深拷贝与本题思路高度一致,也需要记录原节点到新节点的映射
  • 序列化与反序列化:在将复杂对象结构存储后重新还原时,常用类似的两阶段方法

相关推荐
NGC_66112 小时前
Java 死锁预防:从原理到实战,彻底规避并发陷阱
java·开发语言
卓怡学长2 小时前
m277基于java web的计算机office课程平台设计与实现
java·spring·tomcat·maven·hibernate
季明洵2 小时前
Java简介与安装
java·开发语言
沉鱼.442 小时前
枚举问题集
java·数据结构·算法
林夕sama2 小时前
多线程基础(五)
java·开发语言·前端
Zzxy2 小时前
HikariCP连接池
java·数据库
罗超驿2 小时前
Java数据结构_栈_算法题
java·数据结构·
希望永不加班2 小时前
SpringBoot 主启动类解释:@SpringBootApplication 到底做了什么
java·spring boot·后端·spring
参.商.2 小时前
【Day43】49. 字母异位词分组
leetcode·golang