bigywater.tech
算法与数据结构 · LeetCode 25

LeetCode 25. K 个一组翻转链表

给定一个链表,每 k 个结点一组进行翻转,返回新的头结点。如果最后剩下的结点不足 k 个,保持原样。它是反转链表的扩展:把链表切成若干段,每段单独反转再接回去。难点全在"接回去":每一组翻转之后,头变成尾、尾变成头,要正确连接前一组的尾和后一组的头。本文用真实数据展示"最后不足 k 个的部分也被翻转"这个常见错误,给出五种语言的实现,并把分组翻转的过程做成分步演示。

5,000
组随机对拍,与暴力解的结果全部一致
6 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。链表在测试里用整数数组表示。基准解按 k 个一段把数组切开,满 k 个的那段翻转,不满的保持原样,是与指针操作完全不同的独立写法。一共 10 组用例,覆盖:官方示例(k = 2 和 k = 3)、k 等于 1、k 等于链表长度、k 大于链表长度、空链表:

真实实测 · 本机执行输出
输入 [[1,2,3,4,5],2]
输出 [2,1,4,3,5]
输入 [[1,2,3,4,5],3]
输出 [3,2,1,4,5]
输入 [[1],1]
输出 [1]
输入 [[1,2,3],1]
输出 [1,2,3]
输入 [[1,2,3],3]
输出 [3,2,1]
输入 [[1,2,3],4]
输出 [1,2,3]

陷阱:不足 k 个的尾部不能翻转

如果把"每 k 个一组"写成"每一段都翻转",最后不足 k 个的那一段也会被翻转,而题目要求它保持原样。下面是这种写法与 solve 的真实结果:

真实实测 · 本机执行输出
# 最后不足 k 个的部分也被翻转的写法
[1, 2, 3, 4, 5], k=3: 尾部也翻转 -> [3, 2, 1, 5, 4]    solve -> [3, 2, 1, 4, 5]
[1, 2, 3, 4, 5], k=2: 尾部也翻转 -> [2, 1, 4, 3, 5]    solve -> [2, 1, 4, 3, 5]
[1, 2, 3], k=5: 尾部也翻转 -> [3, 2, 1]    solve -> [1, 2, 3]
[1, 2, 3, 4, 5, 6], k=3: 尾部也翻转 -> [3, 2, 1, 6, 5, 4]    solve -> [3, 2, 1, 6, 5, 4]

[1, 2, 3, 4, 5] 在 k = 3 时,错误写法把尾部 [4, 5] 翻成了 [5, 4];k = 5 但只有 3 个结点时,错误写法把整个链表翻转了。而 [1, 2, 3, 4, 5] 在 k = 2 时尾部只剩一个结点 5,翻转和不翻转没有区别,[1..6] 在 k = 3 时刚好分成两组、没有尾部,所以这两行两种写法结果相同,这个错误只在有不足 k 个的尾部时才暴露。本文的解法在翻转之前,先从 prev 往后数 k 个,不够就直接返回,保证不翻转尾部。

先数后翻:五种语言的实现

用哑结点和 prev(上一组翻转之后的最后一个结点)。每一轮:先从 prev 往后数 k 个,数到空说明不够,直接结束;够的话,记下这一组的头 start、尾 end、后面的部分 nxt;把 end.next 设为空断开,翻转 start 到 end 这一段;翻转后的头接到 prev.next,start 现在是尾,接回 nxt;prev 移到 start。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_k_group(head, k): dummy = ListNode(0, head) prev = dummy # prev:上一组翻转之后的最后一个结点 while True: end = prev for _ in range(k): # 先数一数:后面够不够 k 个 end = end.next if end is None: return dummy.next # 不够 k 个:保持原样,整个过程结束 start, nxt = prev.next, end.next end.next = None # 把这一组从链表里断开,单独翻转 p, cur = None, start while cur: cur.next, p, cur = p, cur, cur.next prev.next = p # 翻转后 end 成了头 start.next = nxt # 翻转后 start 成了尾,接回后面的部分 prev = start
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[1,2,3,4,5],2]  →  [2,1,4,3,5]
[[1,2,3,4,5],3]  →  [3,2,1,4,5]
[[1],1]  →  [1]
[[1,2,3],1]  →  [1,2,3]
[[1,2,3],3]  →  [3,2,1]
[[1,2,3],4]  →  [1,2,3]
Java · 解法代码class ListNode { int val; ListNode next; ListNode(int v) { val = v; } } class Solution { public ListNode reverseKGroup(ListNode head, int k) { ListNode dummy = new ListNode(0); dummy.next = head; ListNode prev = dummy; // prev:上一组翻转之后的最后一个结点 while (true) { ListNode end = prev; for (int i = 0; i < k; i++) { // 先数一数:后面够不够 k 个 end = end.next; if (end == null) return dummy.next; // 不够 k 个:保持原样,整个过程结束 } ListNode start = prev.next, nxt = end.next; end.next = null; // 把这一组从链表里断开,单独翻转 ListNode p = null, cur = start; while (cur != null) { ListNode t = cur.next; cur.next = p; p = cur; cur = t; } prev.next = p; // 翻转后 end 成了头 start.next = nxt; // 翻转后 start 成了尾,接回后面的部分 prev = start; } } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[1,2,3,4,5],2]  →  [2,1,4,3,5]
[[1,2,3,4,5],3]  →  [3,2,1,4,5]
[[1],1]  →  [1]
[[1,2,3],1]  →  [1,2,3]
[[1,2,3],3]  →  [3,2,1]
[[1,2,3],4]  →  [1,2,3]
Go · 解法代码type ListNode struct { Val int Next *ListNode } func reverseKGroup(head *ListNode, k int) *ListNode { dummy := &ListNode{Next: head} prev := dummy // prev:上一组翻转之后的最后一个结点 for { end := prev for i := 0; i < k; i++ { // 先数一数:后面够不够 k 个 end = end.Next if end == nil { return dummy.Next // 不够 k 个:保持原样,整个过程结束 } } start, next := prev.Next, end.Next end.Next = nil // 把这一组从链表里断开,单独翻转 var p *ListNode for cur := start; cur != nil; { t := cur.Next cur.Next = p p = cur cur = t } prev.Next = p // 翻转后 end 成了头 start.Next = next // 翻转后 start 成了尾,接回后面的部分 prev = start } }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[1,2,3,4,5],2]  →  [2,1,4,3,5]
[[1,2,3,4,5],3]  →  [3,2,1,4,5]
[[1],1]  →  [1]
[[1,2,3],1]  →  [1,2,3]
[[1,2,3],3]  →  [3,2,1]
[[1,2,3],4]  →  [1,2,3]
JavaScript · 解法代码class ListNode { constructor(val = 0, next = null) { this.val = val; this.next = next; } } function reverseKGroup(head, k) { const dummy = new ListNode(0, head); let prev = dummy; // prev:上一组翻转之后的最后一个结点 while (true) { let end = prev; for (let i = 0; i < k; i++) { // 先数一数:后面够不够 k 个 end = end.next; if (end === null) return dummy.next; // 不够 k 个:保持原样,整个过程结束 } const start = prev.next, nxt = end.next; end.next = null; // 把这一组从链表里断开,单独翻转 let p = null, cur = start; while (cur) { const t = cur.next; cur.next = p; p = cur; cur = t; } prev.next = p; // 翻转后 end 成了头 start.next = nxt; // 翻转后 start 成了尾,接回后面的部分 prev = start; } }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[1,2,3,4,5],2]  →  [2,1,4,3,5]
[[1,2,3,4,5],3]  →  [3,2,1,4,5]
[[1],1]  →  [1]
[[1,2,3],1]  →  [1,2,3]
[[1,2,3],3]  →  [3,2,1]
[[1,2,3],4]  →  [1,2,3]
C++ · 解法代码struct ListNode { int val; ListNode* next; ListNode(int v) : val(v), next(nullptr) {} }; class Solution { public: ListNode* reverseKGroup(ListNode* head, int k) { ListNode dummy(0); dummy.next = head; ListNode* prev = &dummy; // prev:上一组翻转之后的最后一个结点 while (true) { ListNode* end = prev; for (int i = 0; i < k; i++) { // 先数一数:后面够不够 k 个 end = end->next; if (!end) return dummy.next; // 不够 k 个:保持原样,整个过程结束 } ListNode *start = prev->next, *nxt = end->next; end->next = nullptr; // 把这一组从链表里断开,单独翻转 ListNode *p = nullptr, *cur = start; while (cur) { ListNode* t = cur->next; cur->next = p; p = cur; cur = t; } prev->next = p; // 翻转后 end 成了头 start->next = nxt; // 翻转后 start 成了尾,接回后面的部分 prev = start; } } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[1,2,3,4,5],2]  →  [2,1,4,3,5]
[[1,2,3,4,5],3]  →  [3,2,1,4,5]
[[1],1]  →  [1]
[[1,2,3],1]  →  [1,2,3]
[[1,2,3],3]  →  [3,2,1]
[[1,2,3],4]  →  [1,2,3]
真实实测 · 本机执行输出
共 10 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 10/10
  Java        通过 10/10
  Go          通过 10/10
  JavaScript  通过 10/10
  C++         通过 10/10

随机对拍

Python 版本另外和按段翻转的数组做了随机对拍,k 取 1 到 6,链表长度 0 到 14,因此包含刚好分完、有不足 k 的尾部、k 大于长度等所有情形:

真实实测 · 本机执行输出
随机对拍 5000 组(长度 0 到 14 的随机链表,k 取 1 到 6(含剩余不足 k 个、k 大于长度的情形)),solve 与 brute 的结果全部一致。

另外四种语言只在上面 10 组固定用例上验证过,没有做大规模随机对拍。这道题每个结点最多被访问两次(数一次、翻一次),是线性的,所以没有做规模对比实验。

分步演示:先数、再翻、再接回

下面的演示用 [1, 2, 3, 4, 5] 和 k = 2。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。最后剩下的 5 不足 2 个,保持原样。

K 个一组翻转链表:[1, 2, 3, 4, 5],k = 2
当前这一组prev(上一组的尾)

    参考与说明

    • 官方题目页:LeetCode 25. K 个一组翻转链表。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:递归写法;官方页面上"只使用 O(1) 额外空间"的进阶要求,本文的迭代解法满足这一点,但没有单独测量内存。
    京ICP备2026067502号-1京公网安备11010502063487号