单链表不能从尾部向前遍历,而目标顺序要求依次取出“头、尾、次头、次尾”。解决办法是:
整个过程只修改节点的 next 指针,不修改节点值,时间复杂度为 O(n),额外空间复杂度为 O(1)。
给定单链表:
将它重新排列为:
不能只交换节点中的值,必须实际调整节点之间的连接关系。函数没有返回值,需要原地修改以 head 开始的链表。
示例 1:
示例 2:
题目约束:
[1, 5 × 10^4] 范围内。1 <= node.val <= 1000。如果把所有节点存入数组,就能用首尾下标交替取节点,但需要 O(n) 额外空间。
要把空间降到 O(1),关键是把链表后半部分反转。假设原链表被拆成:
反转后半部分后得到:
此时两个链表都只需向前遍历,交替取节点就能得到:
因此,本题不是在两个候选之间进行比较,而是通过改变后半部分的遍历方向,将问题转化为两个链表的交替合并。
让 slow 每次走一步,fast 每次走两步:
循环结束时,slow 位于前半部分的最后一个节点:
所以后半部分的节点数始终不会超过前半部分,交替合并时 first 不会提前变成 null。
先保存后半部分的头节点,再将 slow.next 设为 null:
断开非常重要。如果不断开,合并过程中旧连接仍然存在,容易形成环。
随后使用反转链表的标准写法,将 second 指向的链表反转。
每轮分别从两个链表中取出一个节点:
修改指针前必须先保存 firstNext 和 secondNext,否则后续尚未处理的链表会丢失。
以 1 → 2 → 3 → 4 → 5 为例:
| 阶段 | 已确认的重排前缀 | 尚未处理的节点 |
|---|---|---|
| 找到中点并断开 | 空 | 1 → 2 → 3、4 → 5 |
| 反转后半部分 | 空 | 1 → 2 → 3、5 → 4 |
| 合并第 1 轮 | 1 → 5 | 2 → 3、4 |
| 合并第 2 轮 | 1 → 5 → 2 → 4 | 3 |
最终结果:
对于偶数长度 1 → 2 → 3 → 4:
参考实现来源:doocs/leetcode,按本文结构重新整理;原项目采用 CC BY-SA 4.0。
| 阶段 | 对应代码 | 作用 |
|---|---|---|
| 边界处理 | `if (!head | |
| 寻找中点 | while (fast.next && fast.next.next) | 让 slow 停在前半部分末尾 |
| 断开链表 | slow.next = null | 保证两部分互不相连,避免合并时成环 |
| 反转后半部分 | while (second) | 将尾部节点的顺序变为 Ln → Ln-1 → … |
| 交替合并 | 第二个 while (second) | 依次连接前半部分节点和后半部分节点 |
| 输出 | 无显式返回值 | 通过修改 next 指针原地完成重排 |
可以分三步说明:
slow 停在前半部分末尾;断开后,前半部分长度等于后半部分,或比后半部分多一。Ln、Ln-1、Ln-2、…,正好是目标顺序中需要穿插的节点顺序。L0 → Ln → L1 → Ln-1 → …。后半部分耗尽时,所有需要穿插的节点都已处理;如果总节点数为奇数,前半部分剩余的最后一个节点自然位于链表末尾。所以算法最终得到题目要求的重排链表。
slow.next = null 可能保留旧连接并形成环。next,再修改连接。O(n)。寻找中点、反转和合并都只会线性遍历链表。O(1)。只使用有限个指针变量。单链表只能向后遍历。反转以后,原来的尾节点会变成第二条链表的头节点,从而能按照 Ln、Ln-1、… 的顺序在线性时间内访问。
fast.next && fast.next.next?这个条件让 slow 停在前半部分的最后一个节点。对于奇数长度,前半部分多一个节点;对于偶数长度,两部分等长,因此合并时后半部分不会比前半部分更长。
slow.next = null?它将两部分彻底分开。若保留原来的连接,反转和交替合并时可能让某个节点同时指向已处理部分和未处理部分,最终形成环。
每轮开始前,已经合并的前缀符合 L0 → Ln → L1 → Ln-1 → …;first 和 second 分别指向两部分中下一个尚未合并的节点。
second?拆分后,后半部分的长度不会超过前半部分。只要后半部分还有节点,前半部分就一定有可与之配对的节点;后半部分耗尽时重排已经完成。
可以把所有节点放进数组,再用左右下标交替重连。时间复杂度仍为 O(n),但额外空间复杂度变为 O(n),优点是实现更直观。
fast && fast.next?不能只替换循环条件而保持其他代码不变。对于偶数长度链表,新条件会让 slow 停在后半部分的第一个节点,而当前代码需要它停在前半部分的最后一个节点。如果采用新条件,还需要用额外指针记录 slow 的前驱,并从前驱位置断开链表。
O(n) 吗?不能。单链表没有尾指针和反向指针,要找到尾部节点至少需要从头遍历一次,因此最坏情况下存在 Ω(n) 的时间下界。当前实现已经达到渐进最优。
slow 停在错误位置,导致后半部分比前半部分长。next 之前没有保存后继节点,导致剩余链表丢失。void,错误地依赖返回值获取结果。先只回答第 1 题,再展开后续问题:
slow 最终停在哪个节点?两部分各有多少个节点?slow.next = null,在哪个示例上会出现错误?画出错误连接形成的环。