LeetCode 143重排链表:三步法拆解链表基础操作
发布时间:2026/10/2 1:10:52来源:尧图网络
LeetCode 热门100题里143 重排链表是我刷链表板块时翻车最惨的一道。题面很短就一句话把 L0 → L1 → … → Ln-1 → Ln 重排成 L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …。看着像一个简单的指针游戏结果我第一次提交直接超时第二次死循环第三次才磕磕绊绊通过。后来我把这道题拆开看才发现它本质上是“找中点 反转链表 合并链表”三个基础操作的组合版任何一个环节没吃透都会在这里原形毕露。这篇文章我会把完整思路、每一步的代码写法、边界条件、常见翻车点全部拆开讲清楚适合正在刷热门100题、链表题老是靠背模板但换个场景就懵的读者。1. 题目复盘143题到底在考什么1.1 题意拆解与核心考点先看题目本身。给定一个单链表头节点是 L0最后一个节点是 Ln要求重新排列成“头、尾、第二个头、倒数第二个头……”这种交替顺序。题目给了两个明确约束不能修改节点的 val只能改 next 指针空间复杂度最好控制在 O(1) 的额外空间也就是原地重排。举个直观的例子。输入是 1 → 2 → 3 → 4输出应该是 1 → 4 → 2 → 3。输入是 1 → 2 → 3 → 4 → 5输出应该是 1 → 5 → 2 → 4 → 3。可以看到前一半节点还是保持相对顺序只是被后一半节点“插空”了后一半节点则是完全倒过来插进去的。这就是“重排”这个动作的本质前半段保持原序后半段逆序然后两条链交替合并。所以 143 题的考点非常清晰拆出来就是三件事找到链表的中点把链表分成前后两半把后半段链表反转把前半段和反转后的后半段交替拼起来。这个拆解不是事后诸葛而是做题时就应该有的反应。链表题最怕的就是一上来就想怎么移动多个节点的指针其实任何复杂的链表操作大概率都能拆成若干个已经学过的子问题。143 就是这样一道把基础操作“缝合”起来的典型题。1.2 为什么说它是“三个基础操作的缝合怪”如果把 143 对应的三个子问题映射到 LeetCode 上的原题你会发现全是老朋友找链表中点是 876 题反转链表是 206 题交替合并有点像 21 题合并两个有序链表的变种只不过 21 题要求有序这里只要求一个正序一个逆序。很多刷题攻略会把 143 放在“链表二星难度”的位置但真正让新手崩溃的不是算法本身而是三个步骤衔接处的细节。比如找中点之后到底该不该断链断早了后半段取不到断晚了合并时指针绕圈。再比如反转后半段时用迭代还是递归迭代写法里 cur.next 被覆盖之前有没有先保存下一个节点这些“衔接处”的坑才是刷 LeetCode 题解时最容易遗漏的东西——网上大部分题解直接甩一段完整代码不会告诉你为什么这里要先保存指针、为什么那里要提前断开。我后来看 LeetCode 周赛 430 相关讨论时也发现大家把这类题目戏称为“基础操作缝合怪”题目不是要你会多高级的算法而是要看你能不能在三四个基础操作的组合里保持头脑清醒。143 就是这个类别里最适合用来练手的一道因为它每一步单独拿出来都很简单合在一起就非常考验对链表指针流动的理解程度。2. 完整解题链路三步法的由来与推导2.1 第一步快慢指针找中点的原理与写法找链表的中点最常见的方案是快慢指针。快指针一次走两步慢指针一次走一步当快指针走到末尾时慢指针正好停在中间。这个方法之所以成立是因为快指针速度是慢指针的两倍相同时间内快指针走的距离是慢指针的两倍那么快指针到终点时慢指针自然走到了链表一半的位置。但在写代码之前有一个关键问题要确定快慢指针的循环条件应该怎么写先看两种常见写法。写法 Aslow, fast head, head while fast.next and fast.next.next: slow slow.next fast fast.next.next写法 Bslow, fast head, head while fast and fast.next: slow slow.next fast fast.next.next这两种写法的差异在于当链表长度为偶数时slow 最终停在“左中点”还是“右中点”。写法 A 中快指针每次判断的是 fast.next 和 fast.next.next所以对于偶数长度链表slow 会停在左侧中间节点的位置写法 B 中slow 会停在右侧中间节点的位置。对 143 题来说我们需要的恰恰是“左侧中间节点”也就是 slow 最后停在前半段的最后一个节点这样 slow.next 才是后半段的头节点。如果用了写法 B得到的 slow 是后半段的头节点那接下来反转后半段时还要多做一步处理。所以这里推荐写法 A它天然让“前半段尾部”和“后半段头部”的边界变得很明确。对应代码if not head or not head.next or not head.next.next: return slow, fast head, head while fast.next and fast.next.next: slow slow.next fast fast.next.next这个提前 return 处理了链表为空、只有一个节点、只有两个节点的极端情况。两个节点时fast.next 存在但 fast.next.next 不存在循环不会执行slow 停在 headslow.next 就是第二个节点整体逻辑依然成立但先 return 可以让后面的代码少一些边界负担。2.2 第二步迭代法反转链表的写法与易错点找到中点后slow.next 就是后半段的头节点。我们需要把 res slow.next 这一整段反转过来。反转链表是 206 题的看家本领迭代法的核心是三指针prev 指向已经反转好的链表头cur 指向当前要处理的节点nxt 保存 cur 原本的下一个节点。每次循环做三件事用 nxt 保存 cur.next把 cur.next 指向 prev把 prev 更新为 curcur 更新为 nxt。写成代码second slow.next slow.next None # 断链 prev None cur second while cur: nxt cur.next cur.next prev prev cur cur nxt很多人在反转这一步翻车原因不是不懂逻辑而是没有理解“断链”的时机。上面代码里有一个非常关键的动作slow.next None必须在反转之前执行。为什么因为如果不把前后两段断开反转后半段时虽然后半段内部的指针会重新指向但 slow 仍然指向原来的 second 节点而 second 经过反转后变成了后半段的尾节点它的 next 最终会变成 None但 slow 的 next 还残留着指向它的引用。等到第三步合并时链表中就会出现两条路径同时指向同一个节点的情况最常见的结果就是形成环程序直接卡在死循环里。还有一个细节值得提为什么选择迭代反转而不是递归反转因为递归反转链表虽然代码更简短但递归调用会使用系统栈空间复杂度是 O(n)这和题目的 O(1) 额外空间要求相悖。面试时如果用了递归面试官大概率会追问如何改成迭代写法与其被动被问不如一开始就用迭代。2.3 第三步双链表交叉合并的指针操作细节后半段反转完成后我们拿到了两个链表l1 是前半段头节点就是原链表的 headl2 是反转后的后半段头节点是 prev。这一步要做的是把 l2 的节点逐个插入到 l1 的节点之间。还是以 1 → 2 → 3 → 4 → 5 为例来走一遍。前半段是 1 → 2 → 3后半段反转后是 5 → 4。合并的期望结果是 1 → 5 → 2 → 4 → 3也就是1 的 next 指向 55 的 next 指向原来的 1.next也就是 22 的 next 指向 44 的 next 指向原来的 2.next也就是 33 的 next 指向 None。注意这里每一步都涉及“保存原来的 next”。如果不保存比如直接把 l1.next 指向 l2那么原来 l1 后面的链表就丢了如果不保存 l2.next直接把 l2.next 指向原来的 l1.next那 l2 后面的节点也丢了。所以合并代码的每个循环里必须先记录两个 nextl1, l2 head, prev while l2: nxt1, nxt2 l1.next, l2.next l1.next l2 l2.next nxt1 l1, l2 nxt1, nxt2循环条件是while l2这个条件怎么理解反转后的后半段长度最多和前半段相等奇数长度时后半段比前半段少一个节点所以合并过程中l1 一定不会比 l2 更早耗尽。当 l2 变成 None说明所有后半段节点都已经插入完毕剩下的 l1 节点本来就在链表尾部不需要额外处理整个链表已经重排完成了。到这一步三步法的核心逻辑就完整了。整体代码如下class Solution: def reorderList(self, head: ListNode) - None: if not head or not head.next or not head.next.next: return # 1. 快慢指针找中点 slow, fast head, head while fast.next and fast.next.next: slow slow.next fast fast.next.next # 2. 反转后半段并断开前半段 second slow.next slow.next None prev None cur second while cur: nxt cur.next cur.next prev prev cur cur nxt # 3. 交替合并 l1, l2 head, prev while l2: nxt1, nxt2 l1.next, l2.next l1.next l2 l2.next nxt1 l1, l2 nxt1, nxt2这段代码的时间复杂度是 O(n)因为每个节点最多被访问常数次空间复杂度是 O(1)只用到了几个临时指针变量。3. 边界条件与核心细节调试两天才发现的坑3.1 奇偶长度下的中点处理链表长度奇偶不同找中点后得到的两个链表长度也不同这个差异直接影响合并循环的结束条件和最终形态。先看偶数长度以 1 → 2 → 3 → 4 为例。快慢指针走完之后slow 停在 2前半段是 1 → 2后半段是 3 → 4反转后变成 4 → 3。合并时 l2 长度和 l1 一样所以while l2循环会完整地跑完所有插入步骤最后由 l2 的最后一个节点指向 l1 的剩余部分或者 None结果是 1 → 4 → 2 → 3正确。再看奇数长度以 1 → 2 → 3 → 4 → 5 为例。slow 停在 3前半段是 1 → 2 → 3后半段是 4 → 5反转后变成 5 → 4。l1 有 3 个节点l2 有 2 个节点。合并时 l2 先耗尽循环结束此时 l1 剩余的最后一个节点 3 自动成为链表的末尾结果是 1 → 5 → 2 → 4 → 3也正确。问题的关键点在于奇数长度时 slow 正好是正中间节点这个节点本身不需要参与和后半段的交替插入它永远处于链表的最后一位偶数长度时 slow 是左中节点前半段的最后一个节点最终会指向合并后的链表的倒数第二个节点。理解这一点后你就明白为什么循环条件判断的是 l2 而不是 l1——因为后半段不可能比前半段更长用 l2 作为循环是否结束的标尺是最安全的。3.2 断链时机与指针绕圈前面已经提到了断链的重要性这里再展开说一个常见错误有些人把断链放在了反转之后。比如先反转后半段再执行slow.next None。看起来只是顺序换了一下但实际结果完全不同。反转后半段时slow.next 还指向原来的 second 节点反转过程中 second 变成了新链表的尾节点它的 next 已经指向了 None。此时再执行slow.next None虽然也能把前后两段断开但考虑到 slow 和 second 之间的引用经历了复杂的变化一旦反转部分代码写得不严谨比如没有正确更新最后一个节点的 next就可能残留一条从 slow 到 second 的引用合并时链表就会绕圈。另一个常见的绕圈场景发生在合并阶段。如果你写成了这样while l2: l1.next l2 l2.next l1.next # 此时 l1.next 已经被改成 l2 了 l1 l1.next.next l2 l2.next问题很明显第二行执行后l1.next 已经是 l2第三行再把 l2.next 指向 l1.next就等于让 l2 指向了它自己直接形成一个自环。这个错误在有经验的开发者看来很蠢但在现场调试时非常容易被忽略因为逻辑看着很像“把两个链表交叉连接”实际上却把指针的读取顺序搞反了。正确的做法永远是先保存再修改。3.3 空间复杂度与原地操作的取舍142 题……不对说回 143。有读者可能会问既然找中点、反转、合并这么麻烦能不能用数组先把所有节点存下来然后用双指针重排可以而且代码非常短。class Solution: def reorderList(self, head: ListNode) - None: if not head: return nodes [] cur head while cur: nodes.append(cur) cur cur.next i, j 0, len(nodes) - 1 while i j: nodes[i].next nodes[j] i 1 if i j: break nodes[j].next nodes[i] j - 1 nodes[i].next None数组法的时间复杂度同样是 O(n)但额外空间是 O(n)。LeetCode 的判题器不会因此拒绝你因为题目只要求“原地修改链表”并没有强制空间复杂度。但面试场景完全不同面试官大概率会追问一句“能不能把空间优化到 O(1)”如果你答不出来就说明你对链表指针操作的理解还停留在依赖额外存储的水平。所以我个人的建议是先用数组法理解重排的最终形态再用三步法实现原地版本。这两种方案不矛盾它们在思路上是递进关系——数组法帮助你明确“谁该接谁”原地法帮助你练习“怎么在不能随机访问的情况下完成同样的操作”。4. 从143延伸同类型题与面试变体4.1 同类题对比与进阶路线143 不是孤立的一道题它和链表板块的很多基础题都有千丝万缕的联系。我把相关题目放在一起做了一张对比表能更清楚地看到每道题在技能点上的位置题号题目核心考点与143的关系876链表的中间结点快慢指针143的第一步直接复用206反转链表迭代反转 / 递归反转143的第二步直接复用21合并两个有序链表双指针合并143的第三步是它的变体234回文链表快慢指针 反转链表用到的技巧和143几乎一样143重排链表找中点 反转 合并综合题建议的刷题顺序是先刷 876确认自己能熟练写出快慢指针的两种循环条件再刷 206把迭代反转练到闭眼能写然后刷 21理解双链表合并时指针保存的节奏接着刷 234因为回文链表也需要“找中点 反转后半段”但少了一步合并难度比 143 低一些最后再来啃 143。这样由分解到综合每一步的挫败感都会小很多。另外热榜上还有一个讨论度很高的 073 爱吃香蕉的狒狒也就是 875 题 Koko Eating Bananas。它属于二分答案题和链表是完全不同的技能树。但如果你在准备面试这类二分法基础题也要保持手感不然会在“基础算法四大件”上偏科。我的建议是链表和二分这种题型交叉着刷别连续一个星期只看同一类。4.2 面试中的常见变体与应对面试官如果要考 143通常不会直接甩原题因为原题已经被收录在热门100题里候选人大概率刷过。他们更喜欢在 143 的基础上做变形常见的变形方式有这么几类。第一种变体是“只做前半段的重排”。比如把 1 → 2 → 3 → 4 → 5 → 6 改成 1 → 6 → 2 → 5 → 3 → 4也就是前半段和后半段交替但后半段不反转。这种题目其实比 143 简单只需要把后半段整体移动到前半段的间隙中不需要反转但思路可以复用“找中点 双链表穿插”的框架。第二种变体是“限制不能用递归也不允许修改节点值”。这个限制其实和原题一致真正要考察的是你能不能熟练写出迭代反转以及能不能解释清楚为什么递归版本的空间复杂度不合格。应对方式很简单提前把迭代反转的“三指针模型”写在纸上讲给面试官听边讲边写基本不会出问题。第三种变体是“要求返回一个新的链表不能改动原链表”。这种情况下原地法就不适用了你需要一边遍历原链表一边创建新节点同时维持交替顺序。数组法在这种情况下反而更好用因为你可以先收集节点地址再构建新链表。这也解释了为什么我建议两种方法都要掌握——面试官可以通过变换条件轻松让只会一种解法的人露出短板。还有一种比较进阶的变体是“判断链表是否为回文结构并且要求重排后仍然保持某种性质”。这已经是 234 和 143 的复合题了考察的是组合能力。遇到这种题不要慌还是按那个老套路来找中点 → 反转后半段 → 根据题目要求决定是“比较”还是“合并”。只要基础动作足够熟练组合题本质上是多个步骤的串联。5. 常见报错与排查技巧实录5.1 三道高频报错与现场修复我在本地调试 143 时反复踩过几个典型错误这里直接记录现场版本方便你对照排查。报错一空指针异常发生在快慢指针循环里。很多人会写while fast.next.next:然后被NoneType对象没有属性next这种错误砸脸。原因很简单当链表只有 1 个节点或 fast 已经走到最后一个节点时fast.next 是 None再访问.next就崩了。正确写法是同时判断 fast.next 和 fast.next.nextwhile fast.next and fast.next.next:报错二提交后不报编译错误但显示 Time Limit Exceeded多半是形成了环。最常见的原因是没有在反转前执行slow.next None。想象一下如果前后两段没有断开合并时某个节点可能有两条路径通向同一个后继遍历链表时就永远走不到 None。排查方法是在代码里临时加一个计数器比如遍历到第 100 个节点就强制退出然后打印当前节点值你会很快看到节点值开始重复。报错三结果顺序错乱比如输出是 1 → 5 → 4 → 3 → 2而不是期望的 1 → 5 → 2 → 4 → 3。这种情况通常是合并时保存 next 的顺序出了问题。如果你先保存了 nxt1 却没有保存 nxt2或者保存后更新 l1、l2 时用错了变量就会打乱后半段的原始顺序。修复方式是严格按照“先保存两个 next再修改两个 next 指针最后移动两个遍历指针”的顺序来写不要自作聪明交换步骤。5.2 调试链表题的通用技巧链表是少数“画图比看代码更有效”的题型。遇到 143 这种多步骤题我的调试流程是这样的先用纸笔画一个 5 个节点的链表把每一步执行后的指针状态画出来尤其要标清楚哪些节点的 next 被覆盖了、哪些引用还指向旧位置然后打开本地 Python 环境写一个打印函数每次找完中点、反转完、合并后都打印一遍链表确认形态是否符合预期。一个实用的打印函数def print_list(head): res [] cur head while cur: res.append(str(cur.val)) cur cur.next print( - .join(res))测试用例集至少应该覆盖这几种情况空链表、单个节点、两个节点、三个节点、四个节点、五个节点、含重复值的链表、较长的链表。四种节点数的链表分别对应了奇偶边界和循环边界的测试重复值链表用来检验题目“不改 val 只改指针”的约束是否被遵守。LeetCode 的 Playground 可以直接手写测试用例但说实话调试链表题时本地跑反而更舒服因为你可以随意打印中间状态。我在本地用的就是最简单的 Python 文件加 print 输出不涉及任何复杂工具。链表题不怕代码写得慢就怕不画图直接改改到最后都不知道自己在改哪条边。个人经验与补充技巧最后分享一个我自己的习惯。每次做完 143 这种综合题我都会尝试把代码“重写一遍但不用变量名 l1、l2、prev、cur”而是换成更语义化的名字比如first、second、prevNode、currNode。这个动作看起来毫无意义但能逼着你在脑中重新走一遍指针流动的过程而不是机械地复制粘贴记忆中的代码。我靠这个方法把链表题的稳定性提上了一个台阶。如果 143 你也能一遍通过那恭喜你链表三大基础操作算是真正过关了。后续可以试试 25 题 K 个一组翻转链表、61 题旋转链表它们都是在基础操作之上叠加复杂度的题目思路相通但在细节上又会给你新的惊喜。
网站建设高端定制企业官网