138. 随机链表的复制

题目描述

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

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

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 的情况。如果原节点的 randomnull,复制节点的 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 的边界情况。

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

复制代码
链表题不是只想清楚整体思路就够了,真正写代码时还要特别注意指针修改的先后顺序。
相关推荐
Navigator_Z10 分钟前
LeetCode //C++ - 1226. The Dining Philosophers
c语言·算法·leetcode
FOORIR18 分钟前
3D视觉+AI客流系统怎么做:从Sensor Pipeline到数据事件引擎
算法
Omics Pro2 小时前
Arc研究所Cell|全新虚拟细胞官方基准评测框架
大数据·人工智能·深度学习·算法·机器学习
白狐_7982 小时前
408 数据结构|红黑树插入:只记两大类
数据结构
CoderYanger4 小时前
A.每日一题:1140. 石子游戏 II
java·程序人生·算法·leetcode·游戏·职场和发展·深度优先
小雨笙笙4 小时前
机器学习:评估模型与选择
人工智能·算法·机器学习
高亦真4 小时前
今天是学习嵌入式的第35天
linux·学习·算法
HugoStudio_SWAN4 小时前
洛谷 P1319 / P1320 压缩技术——同一枚硬币的正反面
c++·学习·程序人生·算法
wdfk_prog6 小时前
canopennode-rtt推荐,不只可以做从站,也可以承担主站角色
c语言·开发语言·数据库·学习·算法·深度优先
BioRunYiXue6 小时前
科研干货 | IC50全面解读:概念解析、实验设计与数据分析要点
java·开发语言·javascript·人工智能·算法·数据挖掘·数据分析