LeetCode 92题详解:反转链表II的两种解法与指针操作细节
2026/9/18 3:16:01 网站建设 项目流程

LeetCode 92题是我在刷链表题时觉得最值得反复琢磨的一道。如果你刷过LeetCode 206题(反转整个链表),再来看这道“反转链表II”,会发现它其实是把反转动作限定在一个区间内,难度直接从“入门”跳到了“必须熟练”。面试里这道题出现频率相当高,因为它精准地考察了三个东西:链表节点的指针操作、边界条件的处理、以及代码实现时的手感。很多人在白板上写这道题,思路能说清楚,一动手就崩,崩的地方基本都集中在指针更新顺序和虚拟头节点的处理上。

这篇文章我不打算只贴一个答案了事,而是把这道题彻底拆开,从题目到底在考什么、两种主流解法的推导过程、代码实现细节,到常见错误和扩展变体,一次性讲透。无论你是刚开始刷LeetCode,还是准备面试前突击链表专题,这篇都值得收藏下来反复看。

1. 题目到底在考什么:先看懂LeetCode 92题

1.1 题目描述与输入输出格式

先过一遍原题。给定单链表的头节点head和两个整数leftright,要求反转从位置left到位置right的链表节点,返回反转后的链表。注意这里的leftright是从 1 开始计数的。

举个最经典的例子:

输入:head = [1,2,3,4,5], left = 2, right = 4 输出:[1,4,3,2,5]

也就是说,原始链表是 1->2->3->4->5,把第 2 个节点到第 4 个节点这一小段(2->3->4)反转成 4->3->2,再接回原链表,得到 1->4->3->2->5。

还有一个先决条件:1 <= left <= right <= n,n 是链表长度。这意味着输入不用考虑区间越界的问题。

这道题常见的进阶要求是:一次遍历完成。也就是说你不能先把链表转成数组、反转区间再建链,那样虽然也能过,但面试官基本会追问“能不能一次遍历,空间复杂度 O(1)”,所以直接按最优解来写才是正路。

1.2 这道题为什么是面试高频:考点拆解

很多人刷题喜欢按题号顺序刷,刷到92题时往往已经被前面的链表题折磨得够呛。但我想说的是,92题在整个链表题型里是一个“分水岭”。

它会同时考察你以下这几个能力:

  • 指针操作的精确性。链表反转本质上就是不断修改节点的next指向。全量反转只需要维护两个指针,但区间反转要额外记录区间的“前驱节点”和“后继节点”,相当于在一串珍珠项链里精准地挑出一段,翻转后再接回去,手一抖就散架。
  • 边界条件的敏感度left = 1时,反转区间包含头节点,这时候如果没有虚拟头节点,处理起来会非常别扭;right = n时,区间后面没有节点了,也要保证代码不出错。面试官非常喜欢把这两个边界条件单独拿出来考你。
  • 代码的简洁性和健壮性。这道题解法不止一种,但最优雅的“头插法”只需要一个 for 循环就能完成反转。能在白板上写出简洁且不容易出错的版本,是面试官判断你代码能力的重要依据。

换句话说,刷透这道题,你等于同时复习了 206 题(反转链表)、需要额外处理边界场景的区间操作,也为后面刷 25 题(K 个一组翻转链表)打下了基础。

2. 解题前的关键认知:链表反转的三种形态

在动手写代码之前,有几个底层认知必须先建立起来,不然你只是背代码,换个题目就废。

2.1 从206题到92题:全量反转 vs 区间反转

先回顾一下 206 题的反转逻辑。全量反转链表的经典迭代写法是:

prev = None curr = head while curr: nxt = curr.next curr.next = prev prev = curr curr = nxt return prev

这段代码的核心思路是:每次把当前节点的next指向前一个节点,然后三个指针整体向后移动。循环结束后,prev就是新链表的头节点。

92 题可以看成是 206 题的“局部版本”。差别在于,206 题从头到尾都在反转,而 92 题只反转中间某一段。这就引出了两个额外需求:

  1. 需要找到反转区间的“前驱节点”pre,也就是left位置的前一个节点;
  2. 反转完区间后,需要把区间的头部接回pre,区间的尾部接回原来的后继节点succ

所以整体思路是:定位 -> 反转区间 -> 重新连接。听起来不复杂,但实现细节决定成败。

2.2 虚拟头节点 dummy 为什么必不可少

关于虚拟头节点(dummy node),我见过太多人一开始不理解它的意义,直到在left = 1时把自己卡死。

left = 1时,反转区间从链表头节点开始。此时区间前面没有任何节点,执行“定位 pre”这步时根本没有pre可找。如果强行特判,代码就会变得非常啰嗦。

解决办法就是人为造一个哨兵节点:

dummy = ListNode(-1) dummy.next = head

这个dummy节点不存储有效数据,只用来占位置。这样一来,无论left = 1还是left > 1pre永远都存在。最终返回结果时,只需要返回dummy.next,它一定指向处理完之后链表的真实头节点。

记住一个套路:只要链表操作可能涉及头节点的变更,就无脑加虚拟头节点。这是一个可以帮你省掉大量边界讨论的习惯。

2.3 一次遍历的核心:三指针的接力

92 题的高频进阶要求是一次遍历完成。这意味着你不能先遍历找到right位置再回头处理,而是在从头走到尾的过程中就把反转做掉。

这里要引入一个核心的三指针模型:precurnxt

  • pre永远指向反转区间前一个节点,也就是反转区间的“锚点”,它不会移动;
  • cur始终指向当前待处理区间内的第一个节点(最初是left位置的节点),注意这里说的是“始终”,它不会在循环里推进;
  • nxtcur.next,也就是每次要被“摘走”并移动到区间前端的那个节点。

理解了这个模型,头插法的代码就只剩一个动作:不断把nxtcur后面摘下来,插到pre的后面。这个过程重复right - left次,区间就反转完了。

很多同学死活看不懂头插法的 for 循环,就是没意识到cur在整个过程中一直在向右“后退”——不对,应该说cur的位置没变,变的是它后面挂的节点被逐个摘走。用例子走一遍,立刻就会明白。

3. 两种主流通解:穿针引线与头插法

这道题网上常见的有两种解法:一种是“穿针引线法”,也叫“先定位再断开反转”;另一种是“头插法”。两种思路都值得掌握,因为它们分别对应了不同的思考角度,而且面试中你很可能会被要求说出多种解法。

3.1 方法一:先定位再断链(穿针引线法)

穿针引线法的核心思想比较直白:把反转区间单独拆下来,反转完再接回去。

具体步骤是:

  1. pre指针走到left前一个节点;
  2. 用两个指针left_noderight_node锁定反转区间的左右端点;
  3. 记录区间后面的节点succ
  4. 把区间从原链表上“剪”下来:让pre.next = Noneright_node.next = None
  5. 对区间链表调用 206 题的反转函数,返回新的头节点;
  6. 重新缝合:pre.next = right_node(反转后右端点变成了新区间头),left_node.next = succ

这个方法思路清晰,每一步都对应一个直观的操作,尤其适合在讲解时让别人听懂。但它的缺点是代码较长,需要额外实现一个反转函数,而且断链和重连的过程要非常小心,否则容易丢节点。

3.2 方法二:头插法(推荐解法)

头插法是我个人最推荐的做法,也是面试时写起来最快的版本。它不需要把区间拆下来,而是在一次遍历中,不断把当前节点后面的节点“挪”到pre的后面,效果上和区间反转完全一致。

模拟一下经典用例left = 2, right = 4,链表初始状态是dummy -> 1 -> 2 -> 3 -> 4 -> 5

第一步:pre移动到 1,cur指向 2。此时要把 3 插到 1 的后面,得到dummy -> 1 -> 3 -> 2 -> 4 -> 5

第二步:继续把 4 插到 1 的后面,得到dummy -> 1 -> 4 -> 3 -> 2 -> 5

此时区间2 -> 3 -> 4已经被原地反转成了4 -> 3 -> 2,整个过程只遍历了两个节点,代码里唯一的指针移动就在这几次“摘下和插入”中。

头插法的好处是空间复杂度 O(1),且只做了一次遍历(pre走到左侧端点的那一轮不算额外遍历,因为链表头部操作本身就需要走到定位点)。代码量也不大,后面我会给出完整实现。

3.3 复杂度分析与对比

维度穿针引线法头插法
时间复杂度O(n),需要遍历链表定位O(n),一次遍历完成
空间复杂度O(1),不计递归栈O(1)
代码量较长,需额外反转函数较短,单函数完成
边界处理需要切断再缝合,易漏指针只需要正确管理 pre.next 和 cur.next
面试表现思路直观,但实现易错简洁高效,推荐优先写这个

如果你在 LeetCode 上做题,两个方法都能通过。但如果是在面试现场给你 10 分钟手写,我会毫不犹豫选头插法。

4. 实操过程与代码实现:一步步手写核心解法

光讲思路不行,代码得能跑。这一节我给出头插法的完整实现,并逐步解析每一行的含义。老规矩,以 Python 为主,再给一个 Java 版本做对照。

4.1 Python 版完整代码与逐行解析

# Definition for singly-linked list. # class ListNode: # def __init__(self, val=0, next=None): # self.val = val # self.next = next class Solution: def reverseBetween(self, head: ListNode, left: int, right: int) -> ListNode: dummy = ListNode(-1) dummy.next = head pre = dummy # 第 1 步:让 pre 走到 left 的前一个节点 for _ in range(left - 1): pre = pre.next # cur 指向反转区间的第一个节点,它只负责“向后看” cur = pre.next # 第 2 步:头插法,执行 right - left 次 for _ in range(right - left): nxt = cur.next # 1. 先把 cur 后面的节点摘出来 cur.next = nxt.next # 2. 让 cur 跨过 nxt,直接连到 nxt 后面 nxt.next = pre.next # 3. 让 nxt 指向 pre 后面的第一个节点 pre.next = nxt # 4. 再把 nxt 接到 pre 的后面 return dummy.next

逐行拆解一下这里的重点:

  • dummy节点的作用是统一处理left = 1的情况,不解释太多,直接当固定套路记住。
  • for _ in range(left - 1)predummy开始移动到第left - 1个节点。比如left = 2pre移动 1 步,正好指向节点 1。
  • cur指向pre.next,也就是left位置的那个节点。这个节点在整个过程中不向前移动,每次循环只是把它后面的节点摘走。
  • 循环次数是right - left,这很关键。比如区间长度是 3(left=2, right=4),只需要反转两次,因为第一次把第 3 个节点挪到前面,第二次把第 4 个节点挪到前面,原本第 2 个节点自然就被挤到最后了。

4.2 Java 版本对照实现

/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode reverseBetween(ListNode head, int left, int right) { ListNode dummy = new ListNode(-1); dummy.next = head; ListNode pre = dummy; for (int i = 0; i < left - 1; i++) { pre = pre.next; } ListNode cur = pre.next; for (int i = 0; i < right - left; i++) { ListNode nxt = cur.next; cur.next = nxt.next; nxt.next = pre.next; pre.next = nxt; } return dummy.next; } }

Java 版和 Python 版逻辑完全一致。面试时如果允许选语言,我一般优先写 Python,代码短、不容易写错;但有些公司要求用 Java,那也不慌,逻辑是一样的,只是语法换了皮。

4.3 边界用例验证:left=1 和 right=链表末尾

这段一定要自己推一遍,尤其是left = 1时的执行过程。比如head = [3,5], left = 1, right = 2

  • 初始化:dummy -> 3 -> 5pre = dummy
  • pre移动left - 1 = 0步,所以pre仍然指向dummy
  • cur = pre.next,也就是节点 3;
  • 循环right - left = 1次:
    • nxt = cur.next,即节点 5;
    • cur.next = nxt.next,即节点 5 的 next,为 None,所以3.next = None
    • nxt.next = pre.next,即5.next = 3
    • pre.next = nxt,即dummy.next = 5

最终链表是5 -> 3,返回dummy.next,正确。

再看right = 链表末尾的情况,比如head = [1,2,3], left = 1, right = 3。此时效果等价于整个链表反转,代码同样能正确处理。因为nxt每次摘取的都是cur.next,当区间到达末尾时cur.next会变成 None,nxt.next = pre.next也照样执行,不会产生空指针异常。

5. 常见问题与排查技巧实录

不管我写多少遍这道题,总能在评论区看到几乎相同的几个问题。这里集中整理一下,每个问题都是真实的踩坑经历。

5.1 循环终止条件写错:到底执行几次?

我看到的最常见错误就是把第二个 for 循环的次数写成right - left + 1甚至right - left + 2

记住:反转区间leftright,本质上只需要把区间内除了第一个节点以外的right - left个节点逐个提到最前面。比如 2 到 4,除了第 2 个节点 2 本身,还需要把节点 3 和节点 4 各提一次,所以是 2 次,也就是4 - 2 = 2

如果写成+1,第一次循环后链表就已经多处理了一个节点,后面再循环下去就会把已经反转好的节点又挪动一次,最终结果完全乱掉。

5.2 指针更新顺序记不住:先取 nxt,再动 cur.next

头插法四行代码里,最核心也是最容易出错的一点是:先保存nxt,再修改nxt.nextcur.next。一旦你先改了cur.next,你后面就找不到nxt了,链表直接断掉。

我用一句话记住这个顺序:“先摘后接,先记后改”。具体到代码里就是:

  1. 先记下nxt = cur.next
  2. cur跳过nxtcur.next = nxt.next
  3. nxt指向pre后面的节点;
  4. nxt挂在pre后面。

第 3、4 步的顺序也不能颠倒。如果先执行pre.next = nxt,那pre.next就变成了nxt,此时再执行nxt.next = pre.next,就让nxt.next指向了它自己,形成环。这一步我至少看到五六个人在评论区问过为什么“链表变成环了”,基本都是这个原因。

5.3 易错点速查表

易错点正确姿势
left = 1时没有虚拟头节点一律先建dummy,返回dummy.next
第二个循环写right-left+1严格写right-left
先改cur.next再取nxtnxt = cur.next,再改指针
nxt.nextpre.next的赋值顺序颠倒必须先nxt.next = pre.next,再pre.next = nxt
结束时cur有没有前进?不需要,cur位置不变,变的是它后面连接的节点
返回head还是dummy.next统一返回dummy.next,防止头节点被换掉

6. 从一道题刷穿一类题:链表题的通用套路

刷题最忌讳的就是一题一题地背答案,而是要形成“题型意识”。92 题一旦吃透,你会发现很多题目都是它的变体。

6.1 与25题 K 个一组翻转链表的关联

LeetCode 25 题要求每 K 个节点一组进行翻转。这个题本质上就是反复调用“区间反转”的能力。只需要把链表按 K 个一组划分,对每一组执行区间反转,然后处理好组与组之间的连接即可。

如果 92 题你能裸写出来,25 题你就只需要额外考虑两个点:怎么确定一组的起始位置,以及怎么把上一组的尾巴接到下一组的头上。反过来说,如果 92 题还卡着,25 题基本不会顺畅。

另外,LeetCode 24 题(两两交换链表中的节点)可以看成是 K=2 的 25 题,也是区间反转的一种特殊形态。

6.2 面试中如何快速给出手写代码

面试手写链表题,我有一个固定的节奏:

  1. 先画图,把precurnxt标出来,确定循环步骤;
  2. dummy节点,直接避开头节点变更问题;
  3. 写出定位pre的循环;
  4. 写出头插法的四行核心交换逻辑;
  5. 测两个边界用例:left = 1right = n
  6. 最后再口头跟面试官确认一下时间复杂度和空间复杂度。

这套流程走下来,基本不会翻车。尤其是第 5 步,很多人写出了代码就觉得万事大吉,结果面试官随手改一个参数,代码就崩了。养成“写完就测边界”的习惯,能让你在面试里显得非常专业。

6.3 扩展思考:递归写法与原地算法

92 题除了迭代写法,也可以用递归实现区间反转。递归的核心思想是“递归到 right 位置后开始逐个返回并调整指针”,代码很简洁,但理解成本会高一些。我个人建议优先掌握迭代写法,递归作为进阶内容,面试时如果不问就不主动写。

另外,这道题完全可以纯原地完成,不需要额外开辟数组或者其他数据结构。在 LeetCode 上,这道题的官方题解和热门题解也都强调 O(1) 空间复杂度。遇到任何说“这个题必须用额外空间才能做”的说法,都要打个问号。

把 92 题刷透之后,建议再花一天时间刷掉这几个关联题:206 题(反转整个链表)、24 题(两两交换节点)、25 题(K 个一组翻转)。你会发现这些题之间有一个非常清晰的递进关系,而 92 题正是整个链条里承上启下的关键一环。

我个人在实际练习中的体会是,链表题最考验的就是“指针修改顺序”的肌肉记忆。一道 92 题如果能在不看答案的情况下,15 分钟内手写出来并一次性通过所有测试用例,那么链表这一关你基本算是站稳了。别急着追求刷题数量,先把这道题吃透,后面的路会顺畅很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询