LeetCode 725. 分隔链表
题目描述


题意分析
要把一条单链表切成恰好 k 段,返回一个长度固定为 k 的数组。三条硬约束必须同时满足:任意两段长度之差不超过 1;靠前的段不能比靠后的段短;原链表的相对顺序不能变。最后一条意味着不允许重新组织节点,只能沿着链表从头到尾一刀一刀切下去。
「返回长度为 k 的数组」这句话透露了一个容易被忽略的信号:当链表长度 n 小于 k 时,后面的若干段必须是空链表,而不是把数组缩短。所以结果数组的长度由 k 决定,与 n 无关。
「长度差不超过 1」加上「前面的不短于后面的」这两条合起来,其实已经把每段长度唯一确定了,没有任何自由度。这提示不需要搜索或贪心,只要先把每段该有多长算清楚,剩下的就是纯粹的指针搬运。
边界上要考虑:head 为空(所有段都是空);n 小于 k(前 n 段各 1 个节点,后 k - n 段为空);n 恰好被 k 整除(每段等长,没有多余节点);k 等于 1(整条链表原样返回)。
另外注意「切」是物理切断,不是逻辑标记——每段的尾节点
next必须置空,否则返回的每一段都会一路串到链表末尾。
解法:按长度切分
核心思路
每段长度取决于链表总长度,因此先遍历一次得到
n。设base = n / k、extra = n % k,则前extra段长度为base + 1,其余段长度为base。这样所有段长度只可能相差 1,而且较长段都在前面。第二遍沿原链表切分,不复制节点。处理第
i段前维护不变量:前i段已经按目标长度断开,cur指向第一个尚未分配的节点。记录cur为本段头,前进到本段尾;先保存尾节点的next,再把next置空,最后从保存的位置继续。当
n < k时,base = 0、extra = n,前 n 段各取一个节点,后面自动得到null,无需另写分支。正确性说明:各段长度之和为
extra × (base + 1) + (k - extra) × base = n,所以每个节点恰好分配一次;段长仅为base或base + 1,且较长段在前,满足题目两项长度约束。每次只在段尾断开,不改变节点相对次序。循环结束后得到恰好 k 段,结果正确。
解题步骤
- 遍历链表,统计节点数
n。- 计算
base = n / k、extra = n % k,并创建长度为 k 的结果数组。- 对第
i段,令size = base + 1(i < extra)或base。- 保存段头,从段头再走
size - 1步到段尾;先保存下一段头,再断开当前段。- 重复 k 次并返回结果。
例如
n = 10, k = 3时,base = 3, extra = 1,段长为[4,3,3]。n = 3, k = 5时段长为[1,1,1,0,0],结果后两项为null。空链表则返回 k 个null。
代码实现
class Solution {
public ListNode[] splitListToParts(ListNode head, int k) {
int n = 0;
for (ListNode cur = head; cur != null; cur = cur.next) {
n++;
}
int base = n / k;
int extra = n % k;
ListNode[] parts = new ListNode[k];
ListNode cur = head;
for (int i = 0; i < k; i++) {
parts[i] = cur;
int size = base + (i < extra ? 1 : 0);
for (int j = 1; j < size; j++) {
cur = cur.next;
}
if (cur != null) {
ListNode next = cur.next;
cur.next = null;
cur = next;
}
}
return parts;
}
}
func splitListToParts(head *ListNode, k int) []*ListNode {
n := 0
for cur := head; cur != nil; cur = cur.Next {
n++
}
base, extra := n/k, n%k
parts := make([]*ListNode, k)
cur := head
for i := 0; i < k; i++ {
parts[i] = cur
size := base
if i < extra {
size++
}
for j := 1; j < size; j++ {
cur = cur.Next
}
if cur != nil {
next := cur.Next
cur.Next = nil
cur = next
}
}
return parts
}
复杂度分析
- 时间复杂度:
O(n + k)。统计长度访问 n 个节点;切分阶段总共再访问 n 个节点,并写入 k 个结果位置。- 空间复杂度:
O(1)(不计返回数组)。结果数组占O(k),除此之外只使用固定数量的变量,所有节点原地复用。
关键点总结
- “均分且前长后短”可直接转化为
base = n / k、extra = n % k。- 第 i 段长度统一写成
base + (i < extra ? 1 : 0),避免为n < k单独设计流程。- 指针操作遵守“先保存后继,再断链”,否则会丢失下一段入口。
- 结果长度固定为 k;空段必须以
null/nil保留。
易错点总结
- 前进
size步再断链:[1,2,3,4,5,6], k = 2会把第一段切成 4 个节点。段头已经占一个位置,只需再走size - 1步。- 忘记断开段尾:
[1,2,3,4], k = 2返回的第一段会继续连到 4,看起来仍是整条链表。- 先执行
cur.next = null再读取cur.next:下一段入口立即丢失,后续结果全为null。必须先保存next。- 把余数分给后面的段:
n = 7, k = 3会得到长度[2,2,3],违反前面的段不能更短;正确长度是[3,2,2]。- 结果数组只开
min(n, k)个位置:n = 3, k = 5时会少两个空段,不满足“恰好 k 段”。- 未处理
size = 0就解引用cur:空链表或n < k的尾部空段会触发空指针异常;断链前必须判断cur != null。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 86. 分隔链表 | 中等 | 按值域而非长度划分,需要两条哑结点链表分别串联后拼接,不涉及长度计算 |
| 61. 旋转链表 | 中等 | 同样先统计长度再取模,但要先成环再在指定位置断开,切点由 k 取模决定 |
| 143. 重排链表 | 中等 | 找中点后反转后半段再交替归并,切分只是第一步,重点在反转与合并 |
| 328. 奇偶链表 | 中等 | 按下标奇偶拆成两条链再首尾相接,需要同时推进两个游标而非单游标 |
| 19. 删除链表的倒数第 N 个结点 | 中等 | 用快慢指针一趟定位,避免了先统计长度的第一遍遍历,考察双指针间距 |