bigywater.tech

K 个一组翻转链表:递归版真实爆栈,迭代版怎么写

给一个链表,每 K 个节点为一组整体翻转,最后剩下不足 K 个的原样保留。比如 a→b→c→d→e→f→g→h,k=3,结果是 c→b→a→f→e→d→g→h——前两组各 3 个都翻转了,最后 g,h 只剩 2 个不够一组,原样留着。

真实验证:递归版跑出的结果,和例子完全一致

思路是每组先看够不够 k 个,够的话就把这一组头尾颠倒,再把结果接上"剩下部分递归处理"的结果:

真实实测
def reverse_k_group(head, k):
    node = head
    count = 0
    while node and count < k:
        node = node.next
        count += 1
    if count < k:
        return head          # 不够k个,原样返回

    prev = None
    curr = head
    for _ in range(k):
        nxt = curr.next
        curr.next = prev
        prev = curr
        curr = nxt

    head.next = reverse_k_group(curr, k)   # head已经变成这一组的尾节点
    return prev                             # prev是这一组反转后的头节点

values = ['a','b','c','d','e','f','g','h']
reverse_k_group(build_list(values), k=3)
结果: ['c', 'b', 'a', 'f', 'e', 'd', 'g', 'h'] 和例子完全一致: True

真实验证:8 组边界情况,和暴力法交叉验证

暴力法思路很直接:转成数组,按 k 个一组手动切片、反转,最后不足 k 个的原样拼回去。覆盖几种容易出错的边界:

真实实测
测试用例(输入, k):
  [1..8], k=3   -> 8个节点,k=3,余2个
  [1..9], k=3   -> 9个节点,刚好整除
  [1..5], k=1   -> k=1,应该完全不变
  [1..5], k=5   -> k=5,等于链表长度,整体反转
  [1..5], k=6   -> k=6,超过链表长度,原样不变
  [1..6], k=2   -> k=2,两两反转
  [],     k=3   -> 空链表
  [1],    k=1   -> 单节点
全部 8 组用例,算法结果与暴力法逐一比对: 全部一致 = True

这几个边界情况专门挑了容易出错的场景:k 恰好等于链表长度(整体反转)、k 超过链表长度(整个不反转)、k=1(相当于什么都不做)——全部跟暴力法对上号,没有遗漏。

真实踩坑:递归版在 20 万节点上真实爆栈了

递归版逻辑简洁,但拿一个真实的大链表试一下就露馅了——每处理一组就要递归一层,20 万个节点、k=7,意味着要递归接近 3 万层:

真实实测
N = 200_000
K = 7
print(sys.getrecursionlimit())   # Python 默认递归深度上限

reverse_k_group(build_list(range(N)), K)
当前 Python 递归深度限制: 1000 真实报错: RecursionError: maximum recursion depth exceeded

Python 默认最多递归 1000 层,20 万节点、每 7 个一组要递归近 3 万层,远远超过这个限制,直接真实报错。这不是"理论上可能出问题",是真的跑一下就崩——递归版只适合处理不会太长的链表,生产代码不能这么写。

真实验证:改成迭代版,同样数据 15.9 毫秒跑完

把递归换成一个 group_prev 指针,循环处理每一组,不再依赖调用栈:

真实实测
def reverse_k_group_iter(head, k):
    dummy = ListNode(0, head)
    group_prev = dummy

    while True:
        node = group_prev
        count = 0
        while node.next and count < k:
            node = node.next
            count += 1
        if count < k:
            break
        group_next = node.next

        prev = group_next
        curr = group_prev.next
        while curr != group_next:
            nxt = curr.next
            curr.next = prev
            prev = curr
            curr = nxt

        tmp = group_prev.next
        group_prev.next = prev
        group_prev = tmp

    return dummy.next

reverse_k_group_iter(build_list(range(200_000)), k=7)
结果与暴力法一致: True 耗时: 15.9 ms

同样 20 万节点、k=7,迭代版 15.9 毫秒跑完,不再受调用栈深度限制。另外在能安全跑递归版的小规模数据上(500 个节点,k 从 1 到 501 都测了一遍,包括 k 恰好等于长度和超过长度的边界),两版结果逐一交叉验证,全部一致——说明迭代版不是另一套逻辑,只是把递归换成了循环,正确性完全等价。

一句话总结: 递归版好懂、代码短,但链表一长就有真实的爆栈风险——这不是危言耸听,20 万节点、k=7 真实测出来就崩了。迭代版用一个 group_prev 指针顶替递归调用栈,逻辑完全等价(两版在能对比的规模上交叉验证过),但不会受 Python(或者任何语言)的递归深度限制,链表多长都能处理,是更适合生产环境的写法。
0 / 0

a~h,k=3,按组处理

原始链表
处理结果
还没开始
还没处理 当前这一组 已翻转/保留的结果

当前步骤

—

原始链表:a→b→c→d→e→f→g→h,k=3。点击"下一步"看按组处理的过程。

☕ 如果这篇文章帮到你,可以请作者喝杯咖啡 · 爱发电
京ICP备2026067502号-1京公网安备11010502063487号