【LC 143】重排链表:一个题拆成三道做,中间那道你昨天刚刷过
适用读者:刷过反转链表和快慢指针,但看到「重排」两个字还是不敢下笔的人
技术栈:Python 为主,附 Java 版
难度:中等(单看每步都是简单题,合起来考的是拆问题的能力)
收获:「找中点 + 反转后半段 + 交替合并」这套组合拳,以及那个最容易踩的成环陷阱
题目
给定一个单链表
L:
L0 → L1 → … → Ln-1 → Ln,重新排列为:
L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …。
不能只是修改节点内部的值,须实际改动节点。
输入:head = [1,2,3,4]
输出:[1,4,2,3]
链表节点的定义(后面所有代码都用它):
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
这道题在拼多多 2025 后端面经和美团测开岗都出现过,出现频率比它「中等」的标签暗示的要高。原因很实在:
它是链表三件套的合成题——面试官用它一次能问出你会不会快慢指针、会不会反转、会不会合并,还能顺带看你有没有「先拆后做」的习惯。
而且题目里那句「不能只改值」不是废话,它是整道题的题眼。
一、先把「重排」翻译成指针操作
1.1 结果长什么样
把
[1,2,3,4] 重排成
[1,4,2,3],画出来是这样的:
重排前: 1 → 2 → 3 → 4 → ∅
重排后: 1 → 4 → 2 → 3 → ∅
↑ 从尾巴上拽下来,插进前半段的缝里
规律很直白:
前半段顺序不变,后半段被倒过来,然后一个一个插进前半段的缝隙里。
前半段:1 → 2
后半段:3 → 4 倒过来 → 4 → 3
交替插入:1 → 4 → 2 → 3
再验证奇数长度
[1,2,3,4,5]:前半
1 → 2 → 3,后半
4 → 5 倒过来
5 → 4,交替插入 →
1 → 5 → 2 → 4 → 3。中间那个
3 留在前半段末尾,不动。
1.2 为什么「改值」不算数
如果允许改值,这题会退化成一道数组模拟题:把节点值读进数组,按下标重排,再写回去。全程
next 一个没动 —— 那考的就是数组下标,不是链表。
题目明确要求
实际改动节点,等价于要求你:把后半段的节点
摘下来,再按新顺序
接回去。所以整件事只有三根指针要动,没有一个
val 需要写。
顺带一句:就算题目没写这句要求,面试时也
不要提「用数组存下来再重排」的方案。它的空间是 O(n),而正解能做到 O(1)。主动提它等于自己递把柄。
1.3 一个比喻
把链表想成一队手拉手的小朋友,编号 1 到 n。重排之后要变成:
1号 拉
n号,
n号 拉
2号,
2号 拉
n-1号……
直接让这么多人同时换手肯定乱套。能干的做法是三步走:
- 先找到正中间那个,把队伍一分为二;
- 让后半队集体转身,变成从 n 到中间的顺序;
- 两队交替插人:前队出一个、后队出一个、前队出一个……
这每一步,都恰好是 LeetCode 上已经做过的一道题。
二、拆成三步:每步都是做过的题
这就是这道题的全部难度:看出它其实是三道题。 每一步单独拎出来都是「简单」,合起来标「中等」,标的是「拆」这个动作。
2.1 第 ① 步:中点到底取在哪
用快慢指针:
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
fast 起步比
slow 多一格,配合
while fast and fast.next 这个条件,能让
slow 停在「前半段的最后一个节点」上——这是我们要的位置,因为接下来要从它后面断开。
- 长度 4:
slow 停在 2,前半 1 → 2,后半 3 → 4;
- 长度 5:
slow 停在 3,前半 1 → 2 → 3(中间节点归前半段),后半 4 → 5。
为什么中间节点要归前半段?因为后半段要被反转、再被插进缝隙。如果中间节点进了后半段,它会被插到末尾去;而正确结果里,中间节点应该待在原位不动。这是个很容易搞反的细节——
奇数长度时中点归前半段。
2.2 第 ② 步:断开,然后再反转
second = slow.next
slow.next = None # ← 断开!少这一行就会成环
这一行是整个算法里最不起眼、但最要命的一行。 它把一条链彻底切成了两条互不相干的链。少了它,前半段的尾巴还搭在后半段上,第 ③ 步合并时会把两个指针绕成一个环,链表从此走不到头。
断完之后,反转后半段:
prev, curr = None, second
while curr:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
和 LC 206 逐字一样,只是起点从
head 换成了
second,终点从「走到
None」还是「走到
None」。反转完,
prev 站在新的段头(原来的尾节点)上。
2.3 第 ③ 步:交替合并
first, second = head, prev
while second:
t1, t2 = first.next, second.next # 先把两条链的下家都记住
first.next = second # 前半这个 → 后半这个
second.next = t1 # 后半这个 → 前半的下家
first, second = t1, t2 # 两个指针各自往前
每一轮做两件事:把后半段的一个节点插进来,再把它接回前半段的下一个节点。
循环条件写 while second 就够了——后半段不会比前半段长,它用完了就结束,剩下的前半段尾巴自然带着。
注意
first 不需要判空:后半段每插入一个都要接一个前半节点,
first 和
second 是同步消耗的,而前半段只会更长。
三、动画:看着 4 被拽到 1 后面
下面这个动画把
[1,2,3,4] 的全程拆成了 10 步,三个阶段分成三种颜色标注。留意三件事:
- 找中点:
fast 一次跨两格、slow 一次一格,fast 冲出链表时 slow 正好停在前半段末尾;
- 断开 + 反转:后半段整体下移一行、箭头逐个掉头,
3 的箭头翻过来指回 4;
- 交替合并:后半段的节点一个一个「跳」回上行,插进前半段的缝隙里,指针跟着重连。
看完动画可以数一下指针移动次数:
fast 走
⌊L/2⌋ 次大步,
slow 走同样次数的小步,反转后半段
L/2 个节点,合并再改
L 次
next。
每个节点在整个流程里被访问的次数是常数——这就是 O(L) 的来源。
四、完整代码
4.1 Python
def reorderList(head):
if not head or not head.next:
return # 长度 0/1,无需重排
# ① 找中点:slow 停在前半段的最后一个节点
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# ② 断开 + 反转后半段
second = slow.next
slow.next = None # 断开,否则合并时会成环
prev, curr = None, second
while curr:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
# ③ 交替合并
first, second = head, prev
while second:
t1, t2 = first.next, second.next
first.next = second
second.next = t1
first, second = t1, t2
开头那个 if not head or not head.next 要不要写? 建议写。长度为 1 时后面三步虽然不会崩(
slow 和
fast 都停在唯一节点上,后半段为空,合并循环不进),但显式返回能让意图清楚,也让长度为 0 的输入不至于在
slow.next 上炸掉。面试时说一句「长度 0 和 1 直接返回」,比让它隐式跑通更稳。
4.2 完整推演表
head = [1,2,3,4]:
4.3 Java 版
class Solution {
public void reorderList(ListNode head) {
if (head == null || head.next == null) return;
// ① 找中点
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// ② 断开 + 反转后半段
ListNode second = slow.next;
slow.next = null; // 断开
ListNode prev = null, curr = second;
while (curr != null) {
ListNode nxt = curr.next;
curr.next = prev;
prev = curr;
curr = nxt;
}
// ③ 交替合并
ListNode first = head;
second = prev;
while (second != null) {
ListNode t1 = first.next, t2 = second.next;
first.next = second;
second.next = t1;
first = t1;
second = t2;
}
}
}
注意 Java 版返回值是
void——题目只要求就地重排,不返回新头节点(头节点始终是原来的
head)。这一点和 LC 206 不一样,
别顺手 return head。
五、边界情况
重点看
[1,2,3] 和
[1,2,3,4,5] 这两个奇数长度:
中点归前半段这一条约定,决定了后半段长度一定是
⌊L/2⌋、前半段是
⌈L/2⌉。反过来取,结果会多出一个「中间节点被搬到末尾」的错误。
如果担心自己记反,用
[1,2,3] 验一下就行:正确答案是
1 → 3 → 2(
2 留在原位)。如果跑出来是
1 → 2 → 3 原样没动,说明后半段被判成了空——中点取到
1 上去了。
六、复杂度
空间是 O(1) 的关键在于:
全程只用了 slow/fast/prev/curr/first/second 这几个指针变量,没有开数组、没有用栈、没有递归。递归版反转(如果你手滑写了递归)会立刻把空间抬到 O(L/2),而且长链表有爆栈风险——这也是为什么反转那一步必须写迭代。
七、常见坑
按踩到的频率排序:
1. 忘写 slow.next = None,链表成环
这是本题第一大坑。少了断开,前半段的尾巴还连着后半段;反转之后,后半段的原头节点(现在的尾)指向
None,但前半段尾的
next 依然指着它。合并时指针一绕,就形成一个闭环。
后果很隐蔽:LeetCode 上直接
Time Limit Exceeded(判题机遍历链表走不出去),而不是给你一个清晰的报错。
看到 TLE 先怀疑成环,别急着改算法。
2. 中点取反了,中间节点跑去了后半段
奇数长度时结果会错。判定方法:用
[1,2,3] 自查,正确是
1 → 3 → 2。
3. 反转那步用了递归
空间变 O(L/2),长链表爆栈。迭代写法的四行顺序(
记住 → 掉头 → prev 前移 → curr 前移)必须熟到能默出来。
4. 合并循环写成了 while first
前半段更长,
first 走到
None 时后半段可能还没插完,甚至会空指针。条件必须是
while second——
以后半段为准,它短,它用完就结束。
5. 合并时顺序写反:second.next = t1 写在 first.next = second 前面
# ❌ 顺序错了
second.next = t1 # 先把 second 的下家改了
first.next = second # 但 t1 已经在上一行被用过,语义变了
必须先把两条链的「下家」都存进
t1/t2,再改指针。这也是为什么代码里
t1, t2 = first.next, second.next 一定要写在最前面——
改指针之前先把路记住,和反转链表里那个
nxt = curr.next 是同一个道理。
6. 以为要返回新头节点
头节点从头到尾都是
head,不需要返回。Java 签名是
void,Python 写
return 或不写都行,但别
return head。
八、面试怎么答
- 先复述目标:「前半段不变,后半段倒过来插进缝隙;只能改指针,不能改值。」
- 直接说拆分:「这题拆三步:快慢指针找中点把链切成两段、迭代反转后半段、然后交替合并。」
- 主动点出断开:「找完中点要先
slow.next = None 断开,不然后面会成环。」
- 报复杂度:「三次线性扫描,时间 O(L),全程只用常数个指针,空间 O(1)。」
- 抛出追问:「这三步分别是 LC 876、LC 206、LC 21 的子问题;如果换成数组版本(比如 LC 的『重排数组』变体),思路是一维数组双指针——要展开吗?」
面经里常见的两个追问,提前想好:
- 「为什么不能用数组?」 —— 可以用,但空间 O(n);题目要求实际改动节点,考察的就是原地重排的能力。
- 「合并那步能不能先拼成一条再整体调整?」 —— 不能。交替插入的顺序必须一个一个来,因为每次插入都要改两个
next(前半节点指向后半节点、后半节点指回前半的下家)。
这道题考的不是链表操作,是你能不能把没见过的问题拆成见过的问题。
结语
把三张拼图摆在一起:
组合本身就是解法。 链表题里这种「合成题」不少:LC 25(K 个一组反转)= 206 + 分组连接;LC 143 = 876 + 206 + 21;LC 61(旋转链表)= 快慢指针定位 + 接成环再断开。
所以下次遇到看起来没见过的链表题,先别急着想新算法。问自己两个问题:
- 最终形态能不能拆成几个已知变换的组合?
- 每个变换的接缝在哪(哪个节点要断开、哪个节点要重连)?
把这两问答清楚,剩下的就是三道做过的简单题。
本文的推演动画是页面里的交互组件,可以单步回放。想自己验证的话,拿纸画四个格子,按「切两半 → 后半倒过来 → 一个隔一个插」走一遍——在纸上推一遍,胜过看十遍代码。