全栈开发知识库

62.K 个一组翻转链表

题目

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

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

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

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

示例 2:

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

提示:

  • 链表中的节点数目为 n
  • 1 <= k <= n <= 5000
  • 0 <= Node.val <= 1000

进阶:你可以设计一个只用 O(1) 额外内存空间的算法解决此问题吗?

思路

这一道题其实就是上一题的循环加强版。

上一题是在指定的一个区间做一次反转。

这道题是把整个链表切成一段一段,每段长度是k,然后对每一段都做一次"上一题那样的反转"

所以核心思想就是:

一段一段的处理,每段都用头插法法转,然后把段和段之间连起来。

唯一的新问题就是: 最后一段如果不够k个,就不反转,照样保留,所以每处理一段之前,得先数一数后面还够不够k个节点。

沿用上一题的三个法宝:

  1. dummy哨兵节点 -- 第一段反转时,前段没有节点,dummy帮我们统一处理边界。
  2. pre守门人 -- 每一段都有一个pre,是这段前面那个不动的节点。反转这一段后,pre要更新成这一段的尾巴,为下一段做准备。
  3. 头插法 -- 和上一题一模一样,把段内节点依次摘下插到pre后面。

图解整个过程(head = [1,2,3,4,5], k = 2)

初始,加上 dummy:

pre = 1
dummy → 1 → 2 → 3 → 4 → 5

第一段 [1,2]:数够 2 个,用头插法反转

pre移动这一段的新尾巴,还是1
dummy → 2 → 1 → 3 → 4 → 5

第二段 [3,4]:从新 pre 数够 2 个,反转

dummy → 2 → 1 → 4 → 3 → 5
pre 移动到这一段的新尾巴,这里是3

第三段 [5]:从 pre 往后只有 1 个节点,不够 2 个 → 停止,保持原样

最终结果:

2 → 1 → 4 → 3 → 5 ✅

class Solution {
    fun reverseKGroup(head: ListNode?, k: Int): ListNode? {
        // 1. 哨兵节点,指向 head,统一处理第一段的边界
        val dummy = ListNode(0)
        dummy.next = head

        // pre 是"当前这一段"前面的那个不动的节点,初始是 dummy
        var pre: ListNode = dummy

        while (true) {
            // 2. 先检查:从 pre 出发,后面是否还有 k 个节点
            //    end 走 k 步,如果中途碰到 null,说明不足 k 个
            var end: ListNode? = pre
            for (i in 0 until k) {
                end = end?.next
                if (end == null) {
                    // 剩余节点不足 k 个,保持原样,直接返回
                    return dummy.next
                }
            }

            // 3. 够 k 个,对这一段做头插法反转
            //    cur 是这段的第一个节点,反转过程中它会被挤到段尾
            val cur: ListNode = pre.next!!
            for (i in 0 until k - 1) {
                // 要移动的节点
                val next: ListNode = cur.next!!
                // cur 越过 next(把 next 摘出来)
                cur.next = next.next
                // next 插到 pre 后面
                next.next = pre.next
                // pre 指向 next,完成头插
                pre.next = next                   
            }

            // 4. 这一段反转完成,cur 现在是这段的新尾巴
            //    把 pre 更新为 cur,为处理下一段做准备
            pre = cur
        }
    }
}

复杂度

  • 时间复杂度: O(n):虽然有嵌套循环,但每个节点其实只被"数"一次、被"反转移动"一次,总操作量和节点数成正比。
  • 空间复杂度: O(1):只用了 dummy、pre、cur、end、next 几个指针,没有额外空间。


  • 邮箱 :charon.chui@gmail.com
  • Good Luck!
本页由 Markdown 自动渲染 · 共 628 篇笔记