【力扣-138. 随机链表的复制 ✨】Python笔记

0 阅读5分钟

拒绝“浅拷贝”陷阱:图解随机链表的深拷贝

摘要:本文详解LeetCode 138题“随机链表的复制”。通过巧妙的“拼接-赋值-拆分”三步走策略,在O(1)空间复杂度下实现链表深拷贝,彻底解决random指针指向难题,带你领略链表操作的思维之美。


📚 核心知识点:深拷贝与“时空穿梭”技巧

在处理链表复制问题时,我们通常会遇到一个棘手的难题:random 指针。

普通的链表复制只需要复制 valnext,但 random 指针可能指向链表中的任意节点(甚至是 null)。如果我们直接复制,新节点的 random 就会指向旧节点,这就不符合“深拷贝”的要求了。

为了解决这个问题,通常有两种思路:

  1. 哈希表法(空间换时间) :用字典记录 旧节点 -> 新节点 的映射关系。但这需要 O(N) 的额外空间。
  2. 拼接拆分法(原地操作) :这就是我们要讲的重点!通过巧妙的“穿插”技巧,将空间复杂度降为 O(1)O(1)

核心思想
既然新节点找不到它对应的 random 新节点,那我们就把新节点紧紧贴在旧节点身边!这样,通过旧节点的 random,就能瞬间找到新节点的 random


📝 题目解析:LeetCode 138. 随机链表的复制

题目描述
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。深拷贝应该正好由 n全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点。

简单来说:你要造一个一模一样的克隆链表,但里面的零件全是新的,不能混用旧的。


💡 解题思路:三步走的“魔术”

我们要通过三个步骤,把旧链表变成新链表:

第一步:拼接(插队)
在每个旧节点后面,立刻插入一个克隆的新节点。

  • 旧链表:A -> B -> C
  • 拼接后:A -> A' -> B -> B' -> C -> C'

第二步:赋值(连Random)
利用“近水楼台先得月”的优势,设置新节点的 random

  • 如果 A.random 指向 C,那么 A'.random 就应该指向 C'
  • 因为 C' 就在 C 的旁边(C.next),所以我们可以轻松找到它。

第三步:拆分(分家)
把纠缠在一起的链表拆开,恢复旧链表,提取新链表。

  • 旧链表恢复为:A -> B -> C
  • 新链表提取为:A' -> B' -> C'

💻 代码实现(Python3)

"""
# Definition for a Node.
class Node:
    def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
        self.val = int(x)
        self.next = next
        self.random = random
"""

class Solution:
    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        if not head:
            return None
        
        # ===================== 步骤1:拼接链表,插入新节点 =====================
        # 目的:A -> B -> C  变为  A -> A' -> B -> B' -> C -> C'
        cur = head
        while cur:
            # 1. 创建新节点,值和当前原节点相同
            new_node = Node(cur.val)
            # 2. 新节点的next指向原节点的next(即B)
            new_node.next = cur.next
            # 3. 原节点的next指向新节点(即A'),完成插入
            cur.next = new_node
            # 4. 移动到下一个原节点(跳过刚插入的新节点,即从A跳到B)
            cur = new_node.next
        
        # ===================== 步骤2:给新节点的random指针赋值 =====================
        # 原理:如果 A.random -> C,那么 A'.random 应该 -> C' (即 C.next)
        cur = head
        while cur:
            # 当前原节点对应的新节点(就在原节点旁边)
            new_node = cur.next
            # 如果原节点的random不为空
            if cur.random:
                # 新节点的random = 原节点random的next(对应的克隆节点)
                new_node.random = cur.random.next
            else:
                new_node.random = None
            # 移动到下一个原节点
            cur = new_node.next
        
        # ===================== 步骤3:拆分链表,恢复原链表 + 提取新链表 =====================
        cur = head
        # 新链表的头节点(原头节点的next)
        new_head = head.next
        while cur:
            new_node = cur.next
            
            # 1. 恢复原节点的next指针(跳过新节点,连向后一个原节点)
            cur.next = new_node.next
            
            # 2. 给新节点的next指针赋值(如果有下一个新节点的话)
            # 注意:new_node.next 此时指向的是“下一个原节点”,我们需要跳到“下一个新节点”
            if new_node.next:
                new_node.next = new_node.next.next
            else:
                new_node.next = None
                
            # 3. 移动到下一个原节点
            cur = cur.next
        
        # 返回新链表的头节点
        return new_head

🚀 递归写法:大道至简的“回溯法”

如果你觉得上面的“拼接拆分”像是在变魔术,容易绕晕,那么递归 + 哈希表的方法可能更符合直觉。

核心逻辑
递归的本质是“把大问题拆成小问题”。复制一个节点,取决于复制它的 nextrandom
为了防止无限循环(因为 random 可能指回前面的节点),我们需要一个“备忘录”(哈希表)来记录 旧节点 -> 新节点 的映射。

class Solution:
    # 定义一个哈希表作为缓存,key是旧节点,value是新节点
    cached_node = {}

    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        if not head:
            return None
        
        # 如果这个节点还没被克隆过
        if head not in self.cached_node:
            # 1. 先创建新节点(只赋值val,next和random先空着)
            new_node = Node(head.val)
            # 2. 存入哈希表,标记“这个旧节点我已经处理了”
            self.cached_node[head] = new_node
            # 3. 递归地去克隆它的 next 和 random
            # 这里的精妙之处在于:递归调用会自动处理“是否已存在”的检查
            new_node.next = self.copyRandomList(head.next)
            new_node.random = self.copyRandomList(head.random)
        
        # 如果已经克隆过了,直接返回哈希表里的新节点
        return self.cached_node[head]

递归法总结

  • 优点:逻辑非常清晰,代码短,不需要修改原链表结构。
  • 缺点:需要 O(N) 的额外空间来存哈希表,且递归过深可能导致栈溢出(虽然Python通常能处理几千层)。

📌 总结

  • 迭代法(拼接拆分) :是空间复杂度的王者 O(1)O(1),适合对空间要求严格的场景,面试时写出来说明你对链表操作非常熟练。
  • 递归法(哈希表) :是思维清晰度的王者,代码简洁不易出错,适合快速解题。

希望这篇笔记能帮你彻底搞懂随机链表!下次见~