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 的边界情况。

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

复制代码
链表题不是只想清楚整体思路就够了,真正写代码时还要特别注意指针修改的先后顺序。
相关推荐
过期动态1 小时前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
来一碗刘肉面1 小时前
栈在递归中的应用
数据结构·算法
8K超高清1 小时前
博冠获中国电影电视技术学会科技进步奖
人工智能·科技·算法·安全·接口隔离原则·智能硬件
皓月斯语2 小时前
程序设计语言的特点
开发语言·数据结构·c++
微石科技2 小时前
长期慢病如何做好居家管理?宁波微石科技星梦云康改善周期数据零散短板
大数据·科技·物联网·算法
邪修king2 小时前
C++ 进阶终章:异常机制与智能指针全解 —— 从错误处理到 RAII 资源管理,打通现代 C++ 的核心命脉
android·数据结构·c++
微露清风2 小时前
快速排序算法学习记录
学习·算法·排序算法
梓䈑7 小时前
【算法题攻略】BFS 解决FloodFill 算法、最短路问题、多源BFS 和 拓扑排序
c++·算法·宽度优先