【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. 第二次遍历:根据原节点的 next 和 random 指针,通过哈希表找到对应的新节点,设置新节点的 next 和 random。

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 插入到 cur 和 cur.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.next 和 copy.next,且注意循环中 cur 的移动。

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

七、相关链接

相关推荐
非优秀程序员11 小时前
第05章 播放器大战,暴风、快播与那一夜|《互联网产品恩仇录》
算法
武子康17 小时前
驾驶轨迹换个坐标,就能给移动机器人用吗?
人工智能·算法·机器学习
lisw051 天前
Dayhoff的PAM替换矩阵理论评介!
线性代数·算法·矩阵
一起学安全技术1 天前
拆大模型 03|注意力的一句话版本:不用论文讲清 Attention
深度学习·算法·语言模型·transformer
Delightrs1 天前
Rust LeetCode238 算法题解
开发语言·算法·rust
自信150413057591 天前
重生之从0开始学习c++之——vector-中篇
c++·学习·算法
GoAI2 天前
DeepSeek Harness Memory (一)主流记忆插件总结
后端·算法·面试
Pniubi2 天前
力扣54.螺旋矩阵
算法·leetcode·矩阵