138. 随机链表的复制

题目描述

给定一个带有 next 指针和 random 指针的链表,要求对链表进行深拷贝,并返回复制链表的头节点。

深拷贝的含义是:复制链表中的每个节点都必须是新创建的节点,不能复用原链表节点。新节点的 val 相同,next 和 random 都要指向复制链表中的对应节点。

random 指针可以指向链表中的任意节点,也可以指向 null。复制时,新节点的 random 不能指回原链表节点。

解题思路

这道题可以用"原地穿插复制节点"的方法来做,不需要额外的哈希表记录原节点和复制节点的映射关系。

核心思想是:把每个复制节点插入到原节点的后面。

原链表:

复制代码
1 -> 2 -> 3 -> 4

插入复制节点后:

复制代码
1 -> 1' -> 2 -> 2' -> 3 -> 3' -> 4 -> 4'

这样一来,任意原节点 cur 的复制节点就是 cur.next。如果 cur.random 不为空,那么 cur.random 对应的复制节点就是:

复制代码
cur.random.next

因此可以分三步完成。

第一步:复制节点并插入原节点后面

遍历原链表,对每个原节点 cur 创建一个复制节点 copy,并插入到 cur 后面。

关键点是:修改 cur.next 之前,必须先保存原来的下一个节点。

复制代码
Node nxt = cur.next;
cur.next = copy;
copy.next = nxt;
cur = nxt;

这一步之后,链表会从:

复制代码
1 -> 2 -> 3

变成:

复制代码
1 -> 1' -> 2 -> 2' -> 3 -> 3'

第二步:设置复制节点的 random 指针

穿插完成后,每个原节点的复制节点都在它后面。

如果当前原节点是 cur,那么:

复制代码
cur.next

就是当前节点的复制节点。

如果 cur.random 不为空,那么 cur.random.next 就是 cur.random 对应的复制节点,所以可以让:

复制代码
cur.next.random = cur.random.next;

注意这里只处理 cur.random != null 的情况。如果原节点的 random 是 null,复制节点的 random 保持默认的 null 即可。

第三步:分离原链表和复制链表

最后要把穿插在一起的链表拆开。

拆分前:

复制代码
1 -> 1' -> 2 -> 2' -> 3 -> 3'

拆分后:

复制代码
原链表:1 -> 2 -> 3
复制链表:1' -> 2' -> 3'

分离时要同时维护两条链:

复制代码
原节点指向下一个原节点
复制节点指向下一个复制节点

对于当前原节点 cur:

复制代码
Node copy = cur.next;
Node nextOriginal = copy.next;

其中 nextOriginal 是下一个原节点。

如果 nextOriginal != null,那么下一个复制节点就是:

复制代码
nextOriginal.next

所以复制节点应该指向它:

复制代码
copy.next = nextOriginal.next;

原节点则恢复为指向下一个原节点:

复制代码
cur.next = nextOriginal;

然后继续移动:

复制代码
cur = nextOriginal;

易错点

1. 插入复制节点时不能漏掉最后一个节点

第一轮遍历时,循环条件不能写成:

复制代码
cur != null && cur.next != null

这样会导致最后一个原节点没有被复制。

应该以 cur 是否为空作为结束条件。因为即使当前节点是最后一个节点,也仍然需要为它创建复制节点。

2. 改 next 前必须先保存原 next

链表题里最容易出错的地方是修改指针后,原来的路径丢失。

插入复制节点时,应该先保存:

复制代码
Node nxt = cur.next;

不能写成:

复制代码
Node nxt = copy.next;

因为新创建的 copy.next 初始就是 null。这样会导致只处理完第一个节点,后面的原链表就断了。

这类题要记住一句话:

复制代码
链表题改 next 前,先保存原 next。

3. 空链表要提前处理

如果 head == null,应该直接返回 null。

否则如果直接访问:

复制代码
head.next

会产生空指针异常。

4. 判断条件要先判空,再访问字段

类似下面这种写法是危险的:

复制代码
while (cur.next != null && cur != null)

因为当 cur == null 时,会先访问 cur.next,直接空指针。

应该先判断对象本身是否为空:

复制代码
cur != null && cur.next != null

在这道题的遍历中,很多时候直接以 cur != null 作为循环条件更清晰。

5. 分离链表时要处理最后一个节点

分离链表时:

复制代码
Node nextOriginal = copy.next;

当 cur 是最后一个原节点时,nextOriginal 可能是 null。

所以访问:

复制代码
nextOriginal.next

之前必须先判断 nextOriginal != null。

个人误区记录

第一次写这道题时,我主要卡在"如何分离复制链表和原链表"。

当时已经知道可以把复制节点插入到原节点后面:

复制代码
1 -> 1' -> 2 -> 2' -> 3 -> 3' -> 4 -> 4'

但拆链时没有明确两件事:

复制代码
原节点要指向下一个原节点
复制节点要指向下一个复制节点

后来补充复盘时,又发现自己在实现细节上犯了几个基础错误:

  1. 第一轮循环条件写成 cur != null && cur.next != null,导致最后一个节点不会被复制。
  2. 插入复制节点前没有正确保存 cur.next,而是错用了 copy.next。
  3. 没有先处理 head == null 的情况。
  4. 判断条件顺序错误,可能在 cur == null 时访问 cur.next。
  5. 分离链表时,没有处理 nextOriginal == null 的边界情况。

这道题对我最重要的提醒是:

复制代码
链表题不是只想清楚整体思路就够了,真正写代码时还要特别注意指针修改的先后顺序。
相关推荐
垆边人似月.3 小时前
网格最小代价路径,总代价不超过 budget
算法
虚无的纽扣3 小时前
【力扣刷题】第五天:将 x 减到 0 的最小操作数、最大连续1的个数 III
算法·leetcode·职场和发展
C++ 老炮儿的技术栈3 小时前
char 造成的数据溢出
linux·服务器·c语言·c++·算法·c
彧azz3 小时前
B-树学习总结:从结构特性到核心操作的系统复习
数据结构·笔记·b树·学习
sqc233sqc3 小时前
第 J8 周:ResNeXt-50 算法解析
人工智能·算法
hanlin0313 小时前
刷题笔记:力扣第32题-最长有效括号
笔记·算法·leetcode
All for pursuit.13 小时前
【动态规划-21】312.戳气球
数据结构·c++·算法·leetcode·动态规划
碧海银沙音频科技研究院16 小时前
基于炬芯ATS3089X自研AEC声学回声消除算法
算法
通问AI16 小时前
在两台 Mac Studio 上跑 800B 模型:MLX + RDMA over Thunderbolt 实测与四道门选型法
算法
ZywOo62316 小时前
《算法竞赛进阶指南》0x03 前缀和与差分
算法