138. 随机链表的复制 (Medium)

专题归类: 04-链表 LeetCode 链接: https://leetcode.cn/problems/copy-list-with-random-pointer/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

给你一个长度为 n 的链表,每个节点包含一个额外的随机指针 random,该指针可以指向链表中的任意节点或 null

构造这个链表的深拷贝。深拷贝应该由 n 个全新节点组成,每个新节点的值都设为其对应的原节点的值,新节点的 nextrandom 指针都应指向对应新节点。

示例:

  • 输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
  • 输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

题目详细分析

  • 数据范围: 链表长度 [0, 1000],节点值 [-10^4, 10
  • 输入特征: 每个节点有两个指针:next(指向后继)和 random(指向任意节点或 null)
  • 核心约束: 必须深拷贝——新链表的所有节点都必须是全新创建的,不能与原节点共用
  • 隐藏条件: random 指向的是原链表的节点,而我们需要在新链表中建立对应的引用关系——关键在于建立「原节点 → 克隆节点」的映射
  • 边界条件: 空链表、random 指向 null、random 指向自身、random 形成交叉引用

小白版直白理解

就像你要复印一份文件,但这份文件里有些注释(random 指针)指向文件的其他部分。普通复印只能复制文字(val)和页码顺序(next),但那些「指向其他部分」的注释在新文件里也需要正确指向对应的新位置。

所以我们需要先复制所有页(克隆节点),然后记下每个原页面和它对应的复印页面之间的对应关系,这样才能把注释中的「指向原第 X 页」变成「指向复印版第 X 页」。


解题思路

思路一:三步法(插入克隆 → 设置 random → 拆分,O(1) 空间,推荐)

为什么这样想: 不用哈希表,而是利用原链表的 next 指针来建立映射关系。在每一个原节点后面插入其克隆节点,这样原节点到克隆节点的映射关系天然地通过 next 指针建立起来:原节点.next = 克隆节点。那么原节点.random.next 就是克隆节点的 random 指向。

关键洞察: 克隆节点.random = 原节点.random.next。因为原节点.random 指向原链表的某个节点,该节点的 next 就是对应的克隆节点。这样不需要额外空间就完成了 random 指针的设置。

def copyRandomList(head):
    """三步法:插入克隆 → 设置 random → 拆分"""
    if not head:
        return None
    
    # 第一步:在每个原节点后插入克隆节点
    cur = head
    while cur:
        clone = Node(cur.val)
        clone.next = cur.next
        cur.next = clone
        cur = clone.next  # 跳到原链表的下一个节点
    
    # 第二步:设置克隆节点的 random 指针
    cur = head
    while cur:
        if cur.random:
            cur.next.random = cur.random.next  # 关键!原节点.random 的 next 就是对应的克隆节点
        cur = cur.next.next  # 一次跳两步,跳过克隆节点
    
    # 第三步:拆分链表
    cur = head
    new_head = head.next  # 保存克隆链表的头
    while cur:
        clone = cur.next
        cur.next = clone.next  # 恢复原链表的 next
        if clone.next:
            clone.next = clone.next.next  # 连接克隆链表的 next
        cur = cur.next  # cur 已经是原链表的下一个节点
    
    return new_head

思路二:哈希表法(更直观)

思路讲解: 用字典建立「原节点 → 克隆节点」的映射。第一遍遍历创建所有克隆节点并建立映射;第二遍遍历根据映射设置克隆节点的 next 和 random 指针。思路直观,但需要 O(n) 空间。

def copyRandomList(head):
    """哈希表法"""
    if not head:
        return None
    
    mapping = {}  # 原节点 → 克隆节点
    
    # 第一遍:创建所有克隆节点
    cur = head
    while cur:
        mapping[cur] = Node(cur.val)
        cur = cur.next
    
    # 第二遍:设置 next 和 random
    cur = head
    while cur:
        mapping[cur].next = mapping.get(cur.next)
        mapping[cur].random = mapping.get(cur.random)
        cur = cur.next
    
    return mapping[head]

思路三:回溯 + 哈希表(递归法)

思路讲解: 用递归的方式处理每个节点。如果节点已被克隆则直接从哈希表返回;否则创建克隆节点,递归处理 next 和 random。这种方法天然地处理了可能存在的环(random 指向已访问节点)。

def copyRandomList(head):
    """递归 + 哈希表"""
    mapping = {}
    
    def dfs(node):
        if not node:
            return None
        if node in mapping:
            return mapping[node]
        
        # 创建克隆节点
        clone = Node(node.val)
        mapping[node] = clone
        
        # 递归处理 next 和 random
        clone.next = dfs(node.next)
        clone.random = dfs(node.random)
        
        return clone
    
    return dfs(head)

易错点

  • 三步法的拆分步骤: 拆分时不仅要恢复原链表的 next,还要正确设置克隆链表的 next,两个链表要同时恢复
  • random 为 null 的情况: cur.random.next 会崩溃,必须先判断 if cur.random。哈希表法中用 mapping.get(cur.random) 返回 None 处理
  • 三步法第二步的遍历:cur.next.next 跳过一个偶数步(原→克隆→原→克隆),不是 cur = cur.next
  • 拆分时 clone.next 的判断: 当 clone.next 不存在时,说明到了链表末尾,不能访问 clone.next.next
  • 深拷贝的含义: 新链表的每个节点都是新创建的对象,原链表的修改不应影响新链表

框架提炼

三步法模板: 适用于需要建立原节点和克隆节点一一对应关系的场景。

def copy_list(head):
    if not head:
        return None
    
    # 1. 插入克隆节点
    cur = head
    while cur:
        clone = Node(cur.val)
        clone.next = cur.next
        cur.next = clone
        cur = clone.next
    
    # 2. 设置特殊指针
    cur = head
    while cur:
        if cur.random:
            cur.next.random = cur.random.next
        cur = cur.next.next
    
    # 3. 拆分
    cur = head
    new_head = head.next
    while cur:
        clone = cur.next
        cur.next = clone.next
        if clone.next:
            clone.next = clone.next.next
        cur = cur.next
    return new_head

三步法的核心思想是「借用原链表结构存储映射关系」,避免了额外的哈希表空间。这个思路也可以用于其他需要「复制结构」的问题。


关联题目

  • 206-反转链表 — 链表指针操作基础
  • 146-LRU缓存 — 同样是哈希表 + 链表的数据结构设计题
  • 133-克隆图 — 同样的深拷贝思想,用哈希表建立原对象到克隆对象的映射