LeetCode-092:反转链表 II,找到起点前驱,局部翻转再缝合,就是"穿针引线"
本题在线练习:LeetCode 92. 反转链表 II — 在线练习(免费 · 无需登录 · AI 辅助)
配套刷题网站 Zero2Leetcode —— 内置本地 OJ + AI 教练,零门槛开刷 Hot 100。
一、题目概述
给你单链表的头指针 head 和两个整数 left 和 right,其中 left <= right。请你反转从位置 left 到位置 right 的链表节点,返回反转后的链表。
位置从 1 开始编号。
示例 1
输入:head = [1, 2, 3, 4, 5], left = 2, right = 4
输出:[1, 4, 3, 2, 5]
只反转位置 2 到位置 4 的部分:2 -> 3 -> 4 变成 4 -> 3 -> 2,其余不变。
示例 2
输入:head = [5], left = 1, right = 1
输出:[5]
只有一个节点,且 left == right,不需要反转。
二、这题和 LC206(反转整条链表)的关系
LC206 是反转整条链表,是最基础的链表操作之一。这道题可以看成 LC206 的升级版:
- LC206:反转整条链表
- LC92:只反转链表中间的一段
所以解题的核心思路是:先定位到要反转的区间,对这段子链表做 LC206 一样的反转操作,再把反转后的子链表重新接回原链表。
这个"定位 → 反转 → 重新连接"的过程,就像拿着针线在链表上穿来穿去,所以常被称为"穿针引线"法。
三、核心思路
整个过程分三步:
第一步:找到反转区间的前驱节点
从头开始走 left - 1 步,到达位置 left 的前一个节点,记为 prev。为什么需要它?因为反转完成后,需要用 prev 把反转后的子链表接回去。
为了统一处理 left = 1(头节点就要被反转)的情况,在链表前面加一个哑节点 dummy。
第二步:反转从 left 到 right 的子链表
从位置 left 的节点开始,用迭代法反转 right - left 次。这和 LC206 的反转逻辑一模一样:用 prev、curr、next 三个指针,每次把 curr.next 翻转到 curr 前面。
第三步:重新连接
反转完成后,需要处理两个"接口":
- 反转区间的前驱节点
prev要指向反转后的新头 - 反转区间的原始头节点(现在变成了新尾)要指向反转区间后面的第一个节点
四、代码实现
class Solution:
def reverseBetween(self, head: ListNode, left: int, right: int) -> ListNode:
dummy = ListNode(0)
dummy.next = head
prev = dummy
# 第一步:走到 left 的前一个位置
for _ in range(left - 1):
prev = prev.next
# 第二步:反转 left 到 right 的子链表
curr = prev.next
for _ in range(right - left):
next_node = curr.next
curr.next = next_node.next
next_node.next = prev.next
prev.next = next_node
return dummy.next
五、逐行拆解代码
1. 创建哑节点
dummy = ListNode(0)
dummy.next = head
prev = dummy
dummy 是一个假的头节点,它的 next 指向真正的链表头。这样即使 left = 1(需要反转头节点),prev 也有东西可以指。
2. 找到反转区间的前驱
for _ in range(left - 1):
prev = prev.next
从 dummy 出发走 left - 1 步。走完后,prev 就在位置 left 的前一个节点上。
比如 left = 2,走 1 步:dummy -> 节点1,此时 prev 指向节点 1,它就是位置 2 的前驱。
3. 反转子链表(核心操作)
curr = prev.next
for _ in range(right - left):
next_node = curr.next
curr.next = next_node.next
next_node.next = prev.next
prev.next = next_node
这段代码的思路是"头插法":每次从 curr 后面摘下一个节点,插到反转区间的最前面(prev.next 位置)。
理解这段代码的关键:curr 始终不动,它是反转区间的原始第一个节点。随着反转进行,它会逐渐从区间头变成区间尾。
每一轮循环做四件事:
next_node = curr.next— 取出curr后面的那个节点curr.next = next_node.next— 把next_node从链表中摘出来,curr跳过它指向更后面next_node.next = prev.next— 把next_node插到反转区间的最前面prev.next = next_node— 让前驱指向新的区间头
循环 right - left 次,每次把一个节点从后面摘出来插到前面,最终完成整段反转。
4. 返回结果
return dummy.next
dummy.next 指向最终链表的头节点。如果 left = 1,头节点已经变了,所以不能返回原来的 head。
六、手动模拟
用示例 head = [1, 2, 3, 4, 5],left = 2,right = 4 走一遍。
第一步:找前驱
从 dummy 走 left - 1 = 1 步:
dummy -> 1 -> 2 -> 3 -> 4 -> 5
^
prev 指向节点 1
curr = prev.next,即 curr 指向节点 2。
第二步:反转子链表(循环 right - left = 2 次)
初始状态:
dummy -> 1 -> 2 -> 3 -> 4 -> 5
^ ^
prev curr
第 1 轮:把节点 3 摘出来,插到节点 2 前面
next_node = curr.next # next_node = 节点3
curr.next = next_node.next # 节点2.next = 节点4(跳过节点3)
next_node.next = prev.next # 节点3.next = 节点2(插到前面)
prev.next = next_node # 节点1.next = 节点3(前驱指向新的区间头)
结果:
dummy -> 1 -> 3 -> 2 -> 4 -> 5
^ ^
prev curr(还是节点2,没动)
第 2 轮:把节点 4 摘出来,插到节点 3 前面
next_node = curr.next # next_node = 节点4
curr.next = next_node.next # 节点2.next = 节点5(跳过节点4)
next_node.next = prev.next # 节点4.next = 节点3(插到前面)
prev.next = next_node # 节点1.next = 节点4(前驱指向新的区间头)
结果:
dummy -> 1 -> 4 -> 3 -> 2 -> 5
^ ^
prev curr(还是节点2)
循环结束。
最终结果
1 -> 4 -> 3 -> 2 -> 5
返回 dummy.next,即 [1, 4, 3, 2, 5],和预期一致。
注意观察:curr(节点 2)从头到尾都没有移动过。每一轮都是从 curr 后面摘一个节点出来,插到反转区间的最前面。两轮之后,原来的 2 -> 3 -> 4 就变成了 4 -> 3 -> 2。
七、复杂度分析
时间复杂度:O(n)
- 第一步找前驱:最多走
n步 - 第二步反转子链表:最多走
n步 - 总共遍历链表不超过两次,是线性时间
空间复杂度:O(1)
- 只用了
dummy、prev、curr、next_node几个指针变量,不随链表长度增长
八、总结
这道题的核心洞察是:
局部反转 = 定位前驱 + 头插法反转子链表 + 指针自动缝合
整个解法的关键点:
- 哑节点:在链表前面加
dummy,统一处理left = 1时头节点需要变化的情况 - 头插法:
curr不动,每次把curr.next摘出来插到反转区间的最前面。循环right - left次后,区间内的节点顺序就完全反转了 - 不需要手动缝合:因为
curr始终连着区间后面的节点(每轮的curr.next = next_node.next保证了这一点),而prev.next始终指向区间的新头,所以反转完成后链表自动就是连通的
和 LC206(反转整条链表)对比:
| LC206 | LC92 | |
|---|---|---|
| 反转范围 | 整条链表 | left 到 right |
| 是否需要哑节点 | 不一定 | 需要(统一头节点处理) |
| 是否需要找前驱 | 不需要 | 需要走 left-1 步 |
| 反转手法 | 三指针迭代 | 头插法(本质相同) |
这道题是链表指针操作的经典中等题。把"哑节点 + 定位前驱 + 头插法反转"这个模式吃透,后面遇到 K 个一组翻转链表(LC25)等更难的题目时,就是在这个基础上多套一层循环而已。

浙公网安备 33010602011771号