拒绝“浅拷贝”陷阱:图解随机链表的深拷贝
摘要:本文详解LeetCode 138题“随机链表的复制”。通过巧妙的“拼接-赋值-拆分”三步走策略,在O(1)空间复杂度下实现链表深拷贝,彻底解决random指针指向难题,带你领略链表操作的思维之美。
📚 核心知识点:深拷贝与“时空穿梭”技巧
在处理链表复制问题时,我们通常会遇到一个棘手的难题:random 指针。
普通的链表复制只需要复制 val 和 next,但 random 指针可能指向链表中的任意节点(甚至是 null)。如果我们直接复制,新节点的 random 就会指向旧节点,这就不符合“深拷贝”的要求了。
为了解决这个问题,通常有两种思路:
- 哈希表法(空间换时间) :用字典记录
旧节点 -> 新节点的映射关系。但这需要 O(N) 的额外空间。 - 拼接拆分法(原地操作) :这就是我们要讲的重点!通过巧妙的“穿插”技巧,将空间复杂度降为 。
核心思想:
既然新节点找不到它对应的 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
🚀 递归写法:大道至简的“回溯法”
如果你觉得上面的“拼接拆分”像是在变魔术,容易绕晕,那么递归 + 哈希表的方法可能更符合直觉。
核心逻辑:
递归的本质是“把大问题拆成小问题”。复制一个节点,取决于复制它的 next 和 random。
为了防止无限循环(因为 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通常能处理几千层)。
📌 总结
- 迭代法(拼接拆分) :是空间复杂度的王者 ,适合对空间要求严格的场景,面试时写出来说明你对链表操作非常熟练。
- 递归法(哈希表) :是思维清晰度的王者,代码简洁不易出错,适合快速解题。
希望这篇笔记能帮你彻底搞懂随机链表!下次见~