把两链归并扩展为整体排序和 K 路合并,准确核算栈、堆与输入数组空间。验收:写出常量辅助空间的排序版本。
第六阶段 · 复杂指针与分组反转
138 复制带随机指针的链表 → 25 K 个一组翻转链表
管理多种连接关系和局部区间边界。验收:副本完全独立,原链被恢复,短尾不被错误翻转。
先建立链表的对象模型
ListNode 常见构造方式是 ListNode(val=0, next=None),含 val 与 next 两个属性。head 是指向首节点的引用,空链表用 None 表示;题面 [1,2,3] 是序列化展示,并不是传入函数的 Python 列表。
p = head 只复制引用,不复制节点。p = p.next 移动局部引用;p.next = q 改变节点连接,所有共享该节点的引用都能看到变化。链表随机访问第 i 个节点需要 O(i) 行走,不能套用数组的 O(1) 下标访问直觉。
概念
正确理解
典型使用
值与身份
a.val == b.val 比较值;a is b 比较同一对象。
回文比较值;相交、判环比较身份。
虚拟头 dummy
只为统一头节点变化而存在,不属于输出。
203、21、24、19、148、23、25。
前驱 prev
修改 prev.next 才能跳过目标或接入新组头。
删除节点、两两交换、分组翻转。
尾指针 tail
输出前缀已完成,tail 指向其最后一个节点。
两链归并、两数相加、多路归并。
快慢指针
步速差或固定间距分别解决不同问题。
中点、Floyd;倒数第 N 个固定差距。
深拷贝
新节点的 next、random 都必须指向新节点或 None。
138 随机链表复制。
统一复杂度口径
通常 n 表示链表节点数,m、n 表示两条链的长度,N 表示 K 条链的总节点数;第 19 题用 L 表示长度,避免与删除位置参数 n 混淆。辅助空间不包含输入与必须返回的结果节点,但包含递归栈、集合、映射和堆。 两数相加与深拷贝即使辅助空间 O(1),结果仍需要 O(n) 节点。哈希操作的 O(1) 是平均意义,相关时间标为期望时间。
随后用 cur 检查后继;命中目标时仅跳过后继,未命中时 cur 前进。它与哨兵法保持相同的内部删除逻辑,但需要单独处理头部。
Python 3 · 独立提交
classSolution:defremoveElements(self, head, val):while head isnotNoneand head.val == val:
head = head.next
cur = head
while cur isnotNoneand cur.nextisnotNone:if cur.next.val == val:
cur.next= cur.next.nextelse:
cur = cur.nextreturn head
import sys
sys.setrecursionlimit(max(sys.getrecursionlimit(),20000))classSolution:defreverseList(self, head):if head isNoneor head.nextisNone:return head
new_head =self.reverseList(head.next)
head.next.next= head
head.next=Nonereturn new_head
prev 指向当前待交换二元组的前驱。把两个节点记为 first、second:原结构 prev → first → second → rest。
先让 first 接上 rest,再让 second 接上 first,最后让 prev 接上 second,得到 prev → second → first → rest。first、second 引用保证修改时节点仍可访问。
交换后 first 成为这组尾节点,它就是下一组的前驱。只有两个节点都存在才交换。
Python 3 · 独立提交
classSolution:defswapPairs(self, head):
dummy = ListNode(0, head)
prev = dummy
while prev.nextisnotNoneand prev.next.nextisnotNone:
first = prev.next
second = first.next
first.next= second.next
second.next= first
prev.next= second
prev = first
return dummy.next
把原 head 接到递归结果前,再让 second 指回 head,最后返回 second 作为本组新头。递归栈负责保存各组的上下文。
Python 3 · 独立提交
classSolution:defswapPairs(self, head):if head isNoneor head.nextisNone:return head
second = head.next
head.next=self.swapPairs(second.next)
second.next= head
return second
判断从 head 沿 next 行走是否会再次到达同一节点。若有环返回 True,否则返回 False。pos 只是题面构造尾部连接位置的说明,不传入函数。节点值可以相同,因此值重复不能作为有环依据。
尾节点 −4 指回节点 2,环长为 3;相同值不用于判断环。
解法一:快慢指针 Floyd(推荐)
解题思路与正确性
slow 每轮走一步,fast 每轮走两步;先移动再判断是否是同一个节点,避免把初始同在 head 当作相遇。
若无环,fast 或 fast.next 会先到 None。若有环,两者最终都进入环:每轮 fast 相对 slow 多走一步,在长度为 C 的环上至多 C 轮就会追上。
节点比较用 is 表达身份相同,不比较 val。整个过程只读 next,不修改链表。
Python 3 · 独立提交
classSolution:defhasCycle(self, head):
slow = fast = head
while fast isnotNoneand fast.nextisnotNone:
slow = slow.next
fast = fast.next.nextif slow is fast:returnTruereturnFalse
关键代码解释
关键语句
含义与作用
fast is not None and fast.next is not None
短路判断让 fast.next.next 的读取安全。
if slow is fast
需要同一节点对象;两个值为 1 的不同节点不算相遇。
时间复杂度O(n),n 为从头可达的不同节点数;入环前与环内步数均为线性。
空间复杂度O(1) 辅助空间。
选择建议:满足常量空间要求,是检测函数迭代环的通用方法。
解法二:哈希集合记录访问节点
解题思路与正确性
从 head 依次访问,若当前节点已经在 seen 中,说明沿 next 再次到达了同一节点,存在环。
第一阶段与 Floyd 判环一致:slow 一步、fast 两步,直到相遇或确认无环。相遇后令 finder 从 head 出发,slow 留在相遇点,两者都一步一步走。
证明:设头到入口距离为 a,环长为 C,入口沿环到相遇点距离为 b,慢指针已走 s 步。因为快指针走 2s、两者相遇,所以 s 是 C 的整数倍;同时 s = a+b+qC,于是 a+b ≡ 0 (mod C)。
因此从相遇点再走 a 步恰好回到入口;从 head 走 a 步也到入口。a>0 时 finder 在前 a 步位于环外,不会提前相遇;a=0 时相遇点就是入口。返回两者再次相遇的节点。
Python 3 · 独立提交
classSolution:defdetectCycle(self, head):
slow = fast = head
while fast isnotNoneand fast.nextisnotNone:
slow = slow.next
fast = fast.next.nextif slow is fast:
finder = head
while finder isnot slow:
finder = finder.next
slow = slow.nextreturn finder
returnNone
关键代码解释
关键语句
含义与作用
finder = head
只重置一个指针;另一个留在相遇点。
slow = slow.next
第二阶段两个指针速度都为 1,不再一快一慢。
return finder
返回入口对象,不返回它的值或索引。
时间复杂度O(n),判环与定位入口的总步数为 O(a+C)。
空间复杂度O(1) 辅助空间。
选择建议:空间最优;证明需要区分路径长度与环上的余数。
解法二:第一次重复访问就是入口
解题思路与正确性
用 seen 记录走过的节点。从 head 沿 next 行走,环外节点只经过一次,进入环后第一轮访问每个环内节点,再次到达入口时出现第一次重复。
因此遇到已在集合中的节点时直接返回它。若走到 None,则无环。此法不需要计算位置或环长。
Python 3 · 独立提交
classSolution:defdetectCycle(self, head):
seen =set()
cur = head
while cur isnotNone:if cur in seen:return cur
seen.add(cur)
cur = cur.nextreturnNone
slow、fast 从 0 起步,先移动后比较;相遇后 finder 从 0 重新走,slow 也改为一步,第二次相遇的索引就是重复值。此模型只有查数组,无需真的构建节点。
Python 3 · 独立提交
classSolution:deffindDuplicate(self, nums):
slow = fast =0whileTrue:
slow = nums[slow]
fast = nums[nums[fast]]if slow == fast:break
finder =0while finder != slow:
finder = nums[finder]
slow = nums[slow]return finder
classSolution:deffindDuplicate(self, nums):
left, right =1,len(nums)-1while left < right:
mid =(left + right)//2
count =sum(value <= mid for value in nums)if count > mid:
right = mid
else:
left = mid +1return left
关键代码解释
关键语句
含义与作用
sum(value <= mid for value in nums)
生成器逐个计数,不创建 O(n) 的布尔列表。
right = mid
mid 本身可能是重复数,不能跳过。
时间复杂度O(n log n),二分轮数 O(log n),每轮扫描 O(n)。
空间复杂度O(1) 辅助空间。
选择建议:推理基于抽屉原理,也满足核心限制;速度不及 Floyd。排序或 set 会分别违反不修改或常量空间要求。
示例推演
步骤
操作 / 状态
结果 / 不变量
输入与建模
nums = [1,3,4,2,2]
0 → 1 → 3 → 2 → 4 → 2;入口索引 2
Floyd 第 1、2 轮
(slow,fast) = (1,3)、(3,4)
继续追赶
Floyd 第 3、4 轮
(2,4)、(4,4)
在索引 4 相遇
重置后同步
(finder,slow):(0,4) → (1,2) → (3,4) → (2,2)
返回 2
二分第 1 轮
[1,4],mid=2,count=3>2
收缩到 [1,2]
二分第 2 轮
mid=1,count=1≤1
收缩到 [2,2],输出 2
两种解法对照
解法
时间
辅助空间
取舍
把索引跳转视为链表(推荐)
O(n),可达函数图节点至多 n+1 个。
O(1) 辅助空间,不修改 nums。
同时满足不修改、常量空间和线性时间的要求。
在数值范围上二分计数
O(n log n),二分轮数 O(log n),每轮扫描 O(n)。
O(1) 辅助空间。
推理基于抽屉原理,也满足核心限制;速度不及 Floyd。排序或 set 会分别违反不修改或常量空间要求。
classSolution:defgetIntersectionNode(self, headA, headB):
a, b = headA, headB
while a isnot b:
a = a.nextif a isnotNoneelse headB
b = b.nextif b isnotNoneelse headA
return a
关键代码解释
关键语句
含义与作用
while a is not b
比较节点身份;两个 None 也视为相遇,统一无交点终止。
else headB
到了 None 后才换头,不是在尾节点直接换头。
时间复杂度O(m+n),m、n 为两条链长度。
空间复杂度O(1) 辅助空间。
选择建议:无需预先求长度,且不改变任何 next。
解法二:集合记录 A,再扫描 B
解题思路与正确性
先把 A 的所有节点引用放入集合,再从 B 的头开始扫描。B 中首个属于集合的节点就是共同后缀的入口。
因为链表无环,两个输入都能走到 None;集合只需要保存一条链,不需要记录节点值或位置。
Python 3 · 独立提交
classSolution:defgetIntersectionNode(self, headA, headB):
seen =set()
cur = headA
while cur isnotNone:
seen.add(cur)
cur = cur.next
cur = headB
while cur isnotNone:if cur in seen:return cur
cur = cur.nextreturnNone
classSolution:defisPalindrome(self, head):if head isNoneor head.nextisNone:returnTruedefreverse(node):
prev =Nonewhile node isnotNone:
nxt = node.next
node.next= prev
prev, node = node, nxt
return prev
slow = fast = head
while fast.nextisnotNoneand fast.next.nextisnotNone:
slow = slow.next
fast = fast.next.next
second = reverse(slow.next)
left, right = head, second
result =Truewhile right isnotNone:if left.val != right.val:
result =Falsebreak
left, right = left.next, right.next
slow.next= reverse(second)return result
关键代码解释
关键语句
含义与作用
while fast.next is not None and fast.next.next is not None
classSolution:defisPalindrome(self, head):
values =[]
cur = head
while cur isnotNone:
values.append(cur.val)
cur = cur.next
left, right =0,len(values)-1while left < right:if values[left]!= values[right]:returnFalse
left +=1
right -=1returnTrue
关键代码解释
关键语句
含义与作用
values.append(cur.val)
回文只关心值的对称性,此处存值是正确的。
left < right
中心元素不必和自身重复比较。
时间复杂度O(n)。
空间复杂度O(n) 辅助空间。
选择建议:思路简单且保持输入不变,但不满足常量空间进阶。
示例推演
步骤
操作 / 状态
结果 / 不变量
输入
[1,2,3,2,1]
奇数长度,中心为 3
找中点
slow 从第 1 个走到第 3 个节点
slow.next 指向后半段 [2,1]
反转后半段
[2,1] → [1,2]
second 指向原尾节点
比较
1 对 1,2 对 2
后半段耗尽,中心 3 不比较
恢复
再次反转 [1,2] 并接回
返回 True,原链仍为 [1,2,3,2,1]
非回文
[1,2,3,4] 比较 1 与 4 即失败
仍先恢复后半段,再返回 False
两种解法对照
解法
时间
辅助空间
取舍
找中点 + 反转后半段 + 比较并恢复(推荐)
O(n),找中点、两次反转、比较的总工作仍为线性。
O(1) 辅助空间,无值数组和递归栈。
满足进阶要求,结束后保留输入结构;实现细节比数组法多。
转成值数组比较
O(n)。
O(n) 辅助空间。
思路简单且保持输入不变,但不满足常量空间进阶。
易错点与边界检查
中点模板和后半段起点必须配套,奇偶边界要分别验证。
若在比较失败时直接 return,会跳过结构恢复。
原题可能有 10⁵ 节点,不适合用线性深度递归反转或递归回文判断。
第五阶段 · 排序与多路归并
把两链归并扩展为整体排序和 K 路合并,准确核算栈、堆与输入数组空间。验收:写出常量辅助空间的排序版本。
classSolution:defsortList(self, head):if head isNoneor head.nextisNone:return head
slow, fast = head, head.nextwhile fast isnotNoneand fast.nextisnotNone:
slow = slow.next
fast = fast.next.next
right = slow.next
slow.next=None
left =self.sortList(head)
right =self.sortList(right)
dummy = ListNode(0)
tail = dummy
while left isnotNoneand right isnotNone:if left.val <= right.val:
tail.next, left = left, left.nextelse:
tail.next, right = right, right.next
tail = tail.next
tail.next= left if left isnotNoneelse right
return dummy.next
classSolution:defsortList(self, head):
length, cur =0, head
while cur isnotNone:
length +=1
cur = cur.nextdefsplit(node, size):if node isNone:returnNonefor _ inrange(size -1):if node.nextisNone:break
node = node.next
rest = node.next
node.next=Nonereturn rest
dummy = ListNode(0, head)
step =1while step < length:
prev, cur = dummy, dummy.nextwhile cur isnotNone:
left = cur
right = split(left, step)
cur = split(right, step)while left isnotNoneand right isnotNone:if left.val <= right.val:
prev.next, left = left, left.nextelse:
prev.next, right = right, right.next
prev = prev.next
prev.next= left if left isnotNoneelse right
while prev.nextisnotNone:
prev = prev.next
step *=2return dummy.next
原题现名“随机链表的复制”。节点类 Node 含 val、next、random;random 可指向链上任意节点或 None。创建全新节点,保持 next 与 random 的对应关系,且任何复制指针都不能指回原节点。next 链长度为 0 到 1000,random 可以自指、回指或构成环。
交织结构把“旧节点 → 新节点”的映射藏在 next 中。
解法一:两遍哈希映射(优先掌握)
解题思路与正确性
第一遍只沿 next 走,为每个旧节点创建对应新节点,存入 copies[旧节点]。把 None 也映射到 None,可统一空指针处理。
第二遍仍只沿 next 走,根据已有映射设置新节点的 next 与 random。必须先创建完所有节点,再连接任意指向,才能支持 random 指向尚未遍历到的后方节点。
映射的键必须是旧节点对象而不是值:两个相同 val 的节点也各自需要独立副本。遍历不追随 random,因此 random 中的环不会造成无限递归。
Python 3 · 独立提交
classSolution:defcopyRandomList(self, head):
copies ={None:None}
cur = head
while cur isnotNone:
copies[cur]= Node(cur.val)
cur = cur.next
cur = head
while cur isnotNone:
clone = copies[cur]
clone.next= copies[cur.next]
clone.random = copies[cur.random]
cur = cur.nextreturn copies[head]
关键代码解释
关键语句
含义与作用
copies = {None: None}
空链、空 random、尾部 next 都能直接查表。
copies[cur.random]
取得旧 random 对象对应的新对象,而非直接保留旧引用。
时间复杂度O(n) 期望时间,两遍遍历,字典平均常量时间访问。
空间复杂度O(n) 辅助映射;此外新结果节点占 O(n)。
选择建议:不改动输入结构,容易审查深拷贝正确性。
解法二:原节点旁交织副本,再拆分
解题思路与正确性
第一遍将 A → B → C 交织成 A → A′ → B → B′ → C → C′。旧节点.next 现在就是它的副本,无需映射表。
第二遍只访问旧节点:若旧 random 指向 R,则副本 random 指向 R.next,也就是 R′。此时所有副本已创建完毕,任意方向都可设置。
第三遍同时恢复原链与抽出副本链。先保存 clone.next 中的下一个旧节点,再设置旧 next 和新 next;返回前原链完整恢复。
Python 3 · 独立提交
classSolution:defcopyRandomList(self, head):if head isNone:returnNone
cur = head
while cur isnotNone:
clone = Node(cur.val)
clone.next= cur.next
cur.next= clone
cur = clone.next
cur = head
while cur isnotNone:
clone = cur.next
clone.random = cur.random.nextif cur.random isnotNoneelseNone
cur = clone.next
new_head = head.next
cur = head
while cur isnotNone:
clone = cur.next
nxt = clone.next
cur.next= nxt
clone.next= nxt.nextif nxt isnotNoneelseNone
cur = nxt
return new_head