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 天前
算法竞赛C++常用的STL
c++·算法
小羊没烦恼!3 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
猎头南楼3 天前
知识社区推荐系统实践:新用户冷启动与长短期兴趣建模的挑战 资深推荐算法工程师
人工智能·深度学习·算法·机器学习
m0_547486663 天前
《数据结构教程》全套 PPT课件2026
数据结构
旖旎夜光3 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
wzdark3 天前
大规模并行计算中的负载均衡算法研究4
算法
Because_of_Her13 天前
并查集-听课笔记
笔记·算法·并查集
码流子3 天前
高速公路安全监测实践:碰撞监测预警+物联网底座,从感知到处置的闭环
大数据·人工智能·物联网·算法·架构
another heaven3 天前
【算法/C++ MD5算法能否逆解码?原理、C++实现与同类哈希算法对比】
c++·算法·哈希算法
wzdark3 天前
从算法设计模式看编程思维的抽象能力4
算法