LeetCode-092:反转链表 II,找到起点前驱,局部翻转再缝合,就是"穿针引线"

本题在线练习LeetCode 92. 反转链表 II — 在线练习(免费 · 无需登录 · AI 辅助)

配套刷题网站 Zero2Leetcode —— 内置本地 OJ + AI 教练,零门槛开刷 Hot 100。

一、题目概述

给你单链表的头指针 head 和两个整数 leftright,其中 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 的反转逻辑一模一样:用 prevcurrnext 三个指针,每次把 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 始终不动,它是反转区间的原始第一个节点。随着反转进行,它会逐渐从区间头变成区间尾。

每一轮循环做四件事:

  1. next_node = curr.next — 取出 curr 后面的那个节点
  2. curr.next = next_node.next — 把 next_node 从链表中摘出来,curr 跳过它指向更后面
  3. next_node.next = prev.next — 把 next_node 插到反转区间的最前面
  4. prev.next = next_node — 让前驱指向新的区间头

循环 right - left 次,每次把一个节点从后面摘出来插到前面,最终完成整段反转。


4. 返回结果

return dummy.next

dummy.next 指向最终链表的头节点。如果 left = 1,头节点已经变了,所以不能返回原来的 head


六、手动模拟

用示例 head = [1, 2, 3, 4, 5]left = 2right = 4 走一遍。

第一步:找前驱

dummyleft - 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)

  • 只用了 dummyprevcurrnext_node 几个指针变量,不随链表长度增长

八、总结

这道题的核心洞察是:

局部反转 = 定位前驱 + 头插法反转子链表 + 指针自动缝合

整个解法的关键点:

  1. 哑节点:在链表前面加 dummy,统一处理 left = 1 时头节点需要变化的情况
  2. 头插法curr 不动,每次把 curr.next 摘出来插到反转区间的最前面。循环 right - left 次后,区间内的节点顺序就完全反转了
  3. 不需要手动缝合:因为 curr 始终连着区间后面的节点(每轮的 curr.next = next_node.next 保证了这一点),而 prev.next 始终指向区间的新头,所以反转完成后链表自动就是连通的

和 LC206(反转整条链表)对比:

LC206 LC92
反转范围 整条链表 left 到 right
是否需要哑节点 不一定 需要(统一头节点处理)
是否需要找前驱 不需要 需要走 left-1 步
反转手法 三指针迭代 头插法(本质相同)

这道题是链表指针操作的经典中等题。把"哑节点 + 定位前驱 + 头插法反转"这个模式吃透,后面遇到 K 个一组翻转链表(LC25)等更难的题目时,就是在这个基础上多套一层循环而已。

posted @ 2026-04-20 00:21  孤飞  阅读(53)  评论(0)    收藏  举报