【LeetHOT100】随机链表的复制——Java多解法详解

一、题目描述

138. 随机链表的复制

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 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]]

示例 2:

输入:head = [[1,1],[2,1]]

输出:[[1,1],[2,1]]

示例 3:

输入:head = [[3,null],[3,0],[3,null]]

输出:[[3,null],[3,0],[3,null]]

提示:

  • 0 <= n <= 1000

  • -10⁴ <= Node.val <= 10⁴

  • random 指针为 null 或指向链表中的有效节点。

进阶: 你能用 O(1) 空间解决此问题吗?

二、解题思路概览

本题难点在于随机指针的复制:在创建新节点时,原节点的 random 指针可能指向尚未创建的节点,因此无法一次遍历完成。常见解法有三种:

解法 时间复杂度 空间复杂度 特点
哈希表法(两次遍历) O(n) O(n) 最直观,易于理解
哈希表 + 递归(DFS) O(n) O(n) 代码简洁,利用递归栈
原地复制 + 拆分 O(n) O(1) 面试首选,满足进阶要求

三、解法一:哈希表法(两次遍历)

3.1 思路

  1. 第一次遍历:创建所有新节点,并用一个哈希表 Map<Node, Node> 存储原节点到新节点的映射。

  2. 第二次遍历:根据原节点的 nextrandom 指针,通过哈希表找到对应的新节点,设置新节点的 nextrandom

3.2 代码实现

java

复制代码
class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) return null;
        
        // 1. 建立原节点 -> 新节点的映射
        Map<Node, Node> map = new HashMap<>();
        Node p = head;
        while (p != null) {
            map.put(p, new Node(p.val));
            p = p.next;
        }
        
        // 2. 设置新节点的 next 和 random
        p = head;
        while (p != null) {
            Node newNode = map.get(p);
            newNode.next = map.get(p.next);
            newNode.random = map.get(p.random);
            p = p.next;
        }
        
        return map.get(head);
    }
}

3.3 复杂度分析

  • 时间复杂度:O(n),遍历两次链表。

  • 空间复杂度:O(n),哈希表存储 n 个映射。

四、解法二:哈希表 + 递归(DFS)

4.1 思路

使用递归深度优先遍历,同样利用哈希表记录已复制的节点。递归函数返回当前节点 head 的深拷贝节点,在复制前先检查哈希表中是否已存在,若存在则直接返回。

4.2 代码实现

java

复制代码
class Solution {
    private Map<Node, Node> map = new HashMap<>();
    
    public Node copyRandomList(Node head) {
        if (head == null) return null;
        
        // 如果已经复制过这个节点,直接返回
        if (map.containsKey(head)) {
            return map.get(head);
        }
        
        // 创建新节点,并加入映射
        Node newNode = new Node(head.val);
        map.put(head, newNode);
        
        // 递归复制 next 和 random
        newNode.next = copyRandomList(head.next);
        newNode.random = copyRandomList(head.random);
        
        return newNode;
    }
}

4.3 复杂度分析

  • 时间复杂度:O(n),每个节点被访问常数次。

  • 空间复杂度:O(n),哈希表 + 递归调用栈(最坏深度为 n)。

五、解法三:原地复制 + 拆分(O(1) 空间)⭐

5.1 核心思想

不使用额外哈希表,而是将复制节点直接插入到原节点之后,形成"交错链表",然后一次遍历设置随机指针,最后拆分两条链表。

算法步骤

  1. 克隆节点并插入 :遍历原链表,对每个节点 cur,创建一个新节点 copy,使其 val = cur.val,然后将 copy 插入到 curcur.next 之间。

    • 原:A -> B -> C

    • 变成:A -> A' -> B -> B' -> C -> C'

  2. 设置随机指针 :再次遍历链表,对于每个原节点 cur,其克隆节点 cur.next 的随机指针应指向 cur.random.next(因为 cur.random 的原节点对应其克隆节点)。

    • 注意:如果 cur.random == null,则 cur.next.random = null
  3. 拆分链表 :第三次遍历,将交错链表拆分为原链表和新链表。恢复原链表的 next 关系,同时提取出新链表的 next 关系。

5.2 代码实现

java

复制代码
class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) return null;
        
        // 1. 克隆节点并插入到原节点之后
        Node cur = head;
        while (cur != null) {
            Node copy = new Node(cur.val);
            copy.next = cur.next;
            cur.next = copy;
            cur = copy.next;
        }
        
        // 2. 设置 random 指针
        cur = head;
        while (cur != null) {
            Node copy = cur.next;
            if (cur.random != null) {
                copy.random = cur.random.next;
            }
            cur = copy.next;
        }
        
        // 3. 拆分链表
        Node newHead = head.next;
        cur = head;
        while (cur != null) {
            Node copy = cur.next;
            cur.next = copy.next;
            if (copy.next != null) {
                copy.next = copy.next.next;
            }
            cur = cur.next;
        }
        return newHead;
    }
}

5.3 图解示例

head = [[7,null],[13,0],[11,4],[10,2],[1,0]] 为例(简化示意):

步骤1:克隆插入

text

复制代码
原链表: 7(rand=null) -> 13(rand->7) -> 11(rand->1) -> 10(rand->11) -> 1(rand->7) -> null
克隆插入后:
7 -> 7' -> 13 -> 13' -> 11 -> 11' -> 10 -> 10' -> 1 -> 1' -> null

步骤2:设置 random

  • 7.random = null → 7'.random = null

  • 13.random = 7 → 13'.random = 7'(因为 7 后面紧跟着 7')

  • 11.random = 1 → 11'.random = 1'

  • 10.random = 11 → 10'.random = 11'

  • 1.random = 7 → 1'.random = 7'

步骤3:拆分

  • 取出所有 ' 节点:7' -> 13' -> 11' -> 10' -> 1' -> null

  • 恢复原链表结构:7 -> 13 -> 11 -> 10 -> 1 -> null

5.4 复杂度分析

  • 时间复杂度:O(n),遍历链表三次。

  • 空间复杂度:O(1),只使用了几个指针变量(不计返回的新链表空间)。

六、解法对比与总结

方法 时间复杂度 空间复杂度 是否修改原链表 推荐度
哈希表(两次遍历) O(n) O(n) ⭐⭐⭐⭐
哈希表 + 递归 O(n) O(n) ⭐⭐⭐
原地复制 + 拆分 O(n) O(1) ✅(临时修改,最后恢复) ⭐⭐⭐⭐⭐

6.1 面试建议

  • 首选原地复制 + 拆分法:O(n)+O(1) 空间,满足进阶要求,面试官最期待。

  • 哈希表法容易理解,可作为第一反应,但需说明可优化空间。

  • 注意原地复制法会临时修改原链表,但最终会恢复原状,不影响外部的链表使用。

6.2 常见错误

  1. 克隆节点插入时顺序错误copy.next = cur.next; cur.next = copy; cur = copy.next; 顺序不能颠倒。

  2. 设置 random 时忘记判空cur.random 可能为 null,需要处理。

  3. 拆分链表时丢失新链表的尾部 :需要同时更新 cur.nextcopy.next,且注意循环中 cur 的移动。

  4. 递归解法中未使用缓存:可能导致重复创建节点,造成无限递归或错误。

七、相关链接

相关推荐
AIFarmer1 小时前
【无标题】
开发语言·c++·算法
AGV算法笔记2 小时前
CVPR 2025 最新感知算法解读:GaussianLSS 如何用 Gaussian Splatting 重构 BEV 表示?
算法·重构·自动驾驶·3d视觉·感知算法·多视角视觉
勤劳的进取家3 小时前
数据链路层基础
网络·学习·算法
Advancer-3 小时前
第二次蓝桥杯总结(上)
java·算法·职场和发展·蓝桥杯
ん贤3 小时前
加密算法(对称、非对称、哈希、签名...)
算法·哈希算法
superior tigre4 小时前
78 子集
算法·leetcode·深度优先·回溯
天威?*4 小时前
bitset的数据结构用法
算法·动态规划
hoiii1875 小时前
粒子滤波跟踪系统 - 蒙特卡洛方法实现
算法
weisian1515 小时前
Java并发编程--47-分布式ID生成器:雪花算法(Snowflake)与时钟回拨问题
java·算法·时钟回拨·雪花算法id