我第一次做LeetCode 92这道题用的是最朴素的思路:把要反转的区间整段遍历出来,存成一个数组,然后逆序把数组里的值填回节点。OJ很快给了个Accepted,我甚至觉得这题也不过如此。直到后来面试被追问了一句“如果禁止改val,只能动指针呢”,我才意识到自己绕开了这道题真正要练的核心能力。
LeetCode 92,反转链表II,表面上是206题的加强版,实际上它比206题难了不止一点:206题让你反转整条链表,你只需要一个持续往前挪的指针;92题只反转区间,要求你在正确的位置停下、精准地操作一段子链表、再接回去,任何一个指针提前或延后一步,结果都是环或者丢节点。这篇文章面向正在刷链表专题的人、刚学到单链表的数据结构初学者,以及准备面试但一写链表就心虚的读者。我会从原理讲到代码,再讲几个我实际调试时踩过的边界坑,最后说说这道题在面试和工程里到底在考什么。
1. 先搞懂一件事:这题为什么比206难,难在哪
1.1 会做206,却总是差几行到不了Accepted
206题的核心操作是三指针翻转:pre、cur、next依次挪动,把每个节点的next指向它前面一个节点。这个套路刷过几次之后基本是肌肉记忆,代码量也很短。于是很多人做92时抱着同样的心态,结果一跑就发现不对:边界一多,代码就开始加if-else,加到最后自己都看不懂在干嘛。
打个比方,206题像是把一列火车整列掉头,你只需要考虑车头和车尾;92题像是把这列火车的中间三节车厢摘下来,掉头,再装回去,前后两截车厢还不能动。你需要同时盯住四个位置:区间左边界的前一个节点、左边界节点、右边界节点、右边界后一个节点。任何一个位置的保存时机出错,拼接回去的时候要么形成环,要么把链表拆成两段。
另外一个隐性难点是“返回值”的处理:整链反转时,新的头节点就是原链表的尾节点,很好确定;区间反转时,如果left等于1,头节点本身就是被反转的节点,反转后原来的第二个节点会变成新的头节点,如果代码里还在老老实实返回传入的head,就会输出一段丢了开头的链表。
1.2 头节点身份不稳定,虚拟头节点是必经之路
前面提到,left等于1时,头节点会被卷进反转区间,原来的头节点反转后不再是链表的头。这其实是92题和206题最大的差别之一。206题里,反转完成后的head其实是原来的tail,代码里直接用它做返回值即可;92题则不行,你的返回值可能是原head,也可能是原head之后的某个节点,这取决于反转区间从哪里开始。
为了解决这个问题,最稳的做法是引入虚拟头节点(dummy node):
cpp复制ListNode* dummy = new ListNode(-1);
dummy->next = head;
所有查找和反转操作都从dummy出发,而不是从head出发。最后统一返回dummy->next。这样left等于1时,dummy就充当了原head的“前驱”,让反转逻辑和其他情况完全一致,不需要特判。
我见过不少人排斥虚拟头节点,觉得它是多余的节点,自己多写几个if也能搞定。但我的经验是:在链表题里,虚拟头节点不是“优雅技巧”,而是“保命手段”。它能把你从大量边界条件里解放出来,让你的核心逻辑只有一套,而不是left=1一套、left>1另一套。写过工程代码的人都明白,分支越多,出错概率越高。
1.3 题目在考什么:引用传递和边界控制
很多教程会把92题归为“链表基础题”,但我认为它考的东西相当深:第一,你是否理解单链表的next指针本质上是一条条“引用”,修改它会影响链表整体的结构;第二,你是否能在多指针同时变化时保持逻辑清晰,不弄丢节点;第三,你是否具备边界意识,知道left=1、right=n这类极端情况和普通情况的统一处理方式。
这三点在工程中同样重要。任何涉及到“把某个节点从一条链上摘下来,再插到另一个位置”的操作——比如任务队列重新排序、内存块回收、页面节点重排——底层逻辑都和这道题一模一样。面试官喜欢
