25. K 个一组翻转链表 (Hard)

专题归类: 04-链表 LeetCode 链接: https://leetcode.cn/problems/reverse-nodes-in-k-group/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

给你链表的头节点 head,每 k 个节点一组进行翻转,返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

示例:

  • 输入:head = [1,2,3,4,5], k = 2
  • 输出:[2,1,4,3,5]
  • 输入:head = [1,2,3,4,5], k = 3
  • 输出:[3,2,1,4,5]

题目详细分析

  • 数据范围: 链表中节点数在 [1, 5000] 范围内,节点值在 [0, 1000] 范围内,k ≤ 链表长度
  • 输入特征: 单链表,需要按 K 个一组分段翻转
  • 核心约束: 不能修改节点值,必须实际交换节点。空间复杂度 O(1)
  • 隐藏条件: 难点不在于翻转本身,而在于「组间连接」——翻转后每组的首尾需要与前后组的正确连接
  • 边界条件: k = 1(不需要翻转)、k = 链表长度(全部翻转)、剩余不足 k 个节点(保持原样)

小白版直白理解

还是那串珠子,这次要按每 K 颗一组来翻转子串。

比如每 2 颗一组,珠子颜色是 [红,蓝,绿,黄,紫] → 变成 [蓝,红,黄,绿,紫](最后一颗不够一组不动)。 每 3 颗一组:→ [绿,蓝,红,紫,黄](最后两颗不够一组不动)。

关键在于:每一组翻转之后的「新头」需要接到上一组的「新尾」,这一组的「新尾」要接到下一组的「新头」。就像把几节反转过的链子再连接起来。


解题思路

思路一:迭代法 + 虚拟头节点(推荐)

为什么这样想: 将问题拆分成三个子问题:

  1. 检测当前是否还有 K 个节点(不够则直接返回)
  2. 翻转一个长度为 K 的子链表(复用 206 题的反转逻辑)
  3. 组间重连(将翻转后的组正确地接入主链)

关键洞察: 翻转 K 个节点前,需要记录四个关键位置:前驱节点 pre、当前组头节点 group_head、当前组尾节点(即原 group_head,翻转后变成尾)、下一组的头节点 next_group。翻转后:pre.next = 新头,group_head.next = next_group。

def reverseKGroup(head, k):
    """迭代法:虚拟头节点 + 分组翻转 + 组间重连"""
    if not head or k == 1:
        return head
    
    dummy = ListNode(0, head)
    pre = dummy  # 上一组的末尾(也是当前组的前驱)
    
    while True:
        # 1. 检查是否有 K 个节点
        cur = pre
        for _ in range(k):
            cur = cur.next
            if not cur:
                return dummy.next  # 不足 K 个,结束
        
        # 2. 翻转 K 个节点
        group_head = pre.next   # 当前组的第一个节点(将成为最后一个)
        next_group = cur.next   # 下一组的头节点
        
        # 翻转 [group_head, cur] 这一段
        prev, curr = None, group_head
        for _ in range(k):
            nxt = curr.next
            curr.next = prev
            prev = curr
            curr = nxt
        # 翻转后 prev 是新头,group_head 是尾
        
        # 3. 组间重连
        pre.next = prev          # 前驱指向新头
        group_head.next = next_group  # 当前组尾指向下一组
        
        pre = group_head  # pre 移到当前组尾(即下一组的前驱)
 
 
def reverse(head, k):
    """辅助函数:翻转长度为 K 的子链表,返回新头"""
    pre, cur = None, head
    for _ in range(k):
        nxt = cur.next
        cur.next = pre
        pre = cur
        cur = nxt
    return pre

思路二:递归法

思路讲解: 递归地处理每一组。先检查是否有 K 个节点,有则翻转当前组,然后递归处理下一组,并将当前组的尾节点指向递归结果。

def reverseKGroup(head, k):
    """递归法"""
    if not head:
        return None
    
    # 检查是否有 K 个节点
    cur = head
    count = 0
    while cur and count < k:
        cur = cur.next
        count += 1
    
    if count < k:
        return head  # 不足 K 个,原样返回
    
    # 翻转前 K 个节点
    prev, curr = None, head
    for _ in range(k):
        nxt = curr.next
        curr.next = prev
        prev = curr
        curr = nxt
    
    # 递归处理后续链表
    head.next = reverseKGroup(curr, k)  # head 现在是当前组的尾
    return prev  # prev 是当前组的新头

思路三:使用栈辅助(不推荐,但好理解)

思路讲解: 每次取 K 个节点入栈,然后依次弹出实现翻转。弹出的节点依次接到结果链表后面。最后不足 K 个的节点保持原序接入。

def reverseKGroup(head, k):
    """栈辅助法(空间 O(k))"""
    dummy = ListNode(0)
    cur = dummy
    
    while True:
        stack = []
        temp = head
        # 尝试取 K 个节点
        for _ in range(k):
            if not temp:
                # 不足 K 个,保持原序
                cur.next = head
                return dummy.next
            stack.append(temp)
            temp = temp.next
        
        # 弹出实现翻转
        while stack:
            cur.next = stack.pop()
            cur = cur.next
        
        head = temp  # 指向下一组的头
        cur.next = None  # 断开,防止成环
    
    return dummy.next

易错点

  • 不足 K 个的处理: 剩余不足 K 个时保持原序,不能翻转。检查时机是翻转之前
  • 组间连接: 翻转后 group_head.next 必须指向 next_group,否则链表会在组间断裂
  • pre 的更新: 每次处理完一组后,pre 要更新为 group_head(翻转后的组尾),它是下一组的前驱
  • dummy.next 的返回: 必须返回 dummy.next,因为原 head 可能已不在链首
  • K=1 的优化: k=1 时不需要任何操作,可以直接返回 head
  • 边界: 空链表或 k=1,直接返回 head

框架提炼

K 个一组翻转模板: 将复杂的链表分段操作拆解为标准化的步骤。

# 整体框架
dummy = ListNode(0, head)
pre = dummy
 
while True:
    # 1. 检查是否有 K 个节点
    cur = pre
    for i in range(k):
        cur = cur.next
        if not cur: return dummy.next
    
    # 2. 翻转当前组
    group_head = pre.next
    next_group = cur.next
    pre.next = reverse(group_head, k)  # 前驱指向新头
    group_head.next = next_group       # 组尾指向下一组
    
    # 3. 移动 pre
    pre = group_head
 
# 翻转子函数
def reverse(head, k):
    pre, cur = None, head
    for _ in range(k):
        nxt = cur.next
        cur.next = pre
        pre = cur
        cur = nxt
    return pre

这个模板的核心思想是「先检测、再处理、后重连」,适用于各类需要分段处理链表的问题。


关联题目