题目描述
给定一个带有 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'
但拆链时没有明确两件事:
原节点要指向下一个原节点
复制节点要指向下一个复制节点
后来补充复盘时,又发现自己在实现细节上犯了几个基础错误:
- 第一轮循环条件写成
cur != null && cur.next != null,导致最后一个节点不会被复制。 - 插入复制节点前没有正确保存
cur.next,而是错用了copy.next。 - 没有先处理
head == null的情况。 - 判断条件顺序错误,可能在
cur == null时访问cur.next。 - 分离链表时,没有处理
nextOriginal == null的边界情况。
这道题对我最重要的提醒是:
链表题不是只想清楚整体思路就够了,真正写代码时还要特别注意指针修改的先后顺序。