【LeetCode 热题 100】25. K 个一组翻转链表——迭代+哨兵
Problem: 25. K 个一组翻转链表
题目:给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(N)
- 空间复杂度:O(1)
整体思路
这段代码旨在解决一个高难度的经典链表问题:K 个一组翻转链表 (Reverse Nodes in k-Group)。问题要求将一个链表中的节点每 k
个一组进行翻转,如果最后剩余的节点数不足 k
,则保持原样。
该算法采用了一种精巧的 迭代 方法,通过分组处理和局部反转来完成任务。它结合了 哨兵节点 和多个指针,以清晰地管理复杂的链接关系。
其核心逻辑步骤如下:
-
预计算链表长度:
- 算法首先通过一次遍历计算出链表的总长度
n
。 - 目的:这使得在主循环中可以方便地判断剩余的节点是否还足够形成一个大小为
k
的组,即while (n >= k)
。
- 算法首先通过一次遍历计算出链表的总长度
-
初始化:
- 哨兵节点:创建一个
dummy
节点,其next
指向原始头节点head
。这极大地简化了对链表头部的修改,因为第一组的反转与其他组的逻辑可以完全统一。 - 关键指针:
groupPrev
: 指向当前待反转组的前一个节点。它负责将处理好的前一部分链表与当前反转好的组连接起来。初始时指向dummy
。pre
: 用于在组内进行反转的指针,初始为null
。cur
: 指向在组内当前正在处理的节点,初始为head
。
- 哨兵节点:创建一个
-
分组迭代与反转:
- 算法进入一个主
while
循环,只要剩余节点数n
大于等于k
,就处理一个组。 - 组内反转:
- 在主循环内部,一个
for
循环执行k
次,用于反转当前组的k
个节点。 - 这部分使用了标准的迭代反转链表的技巧:通过
pre
和cur
指针,逐个将cur
节点的next
指向前一个节点pre
,从而实现局部反转。 for
循环结束后,pre
指向了该组反转后的新头节点,而cur
指向了下一组的起始节点。
- 在主循环内部,一个
- 重新链接:
- 这是算法最精妙的部分。反转一个组后,需要将其正确地接回主链表中。
preTail = groupPrev.next
: 首先,找到反转前该组的头节点,这个节点在反转后会成为尾节点。我们将其保存为preTail
。preTail.next = cur
: 将反转后的组的尾部 (preTail
) 连接到下一组的头部 (cur
)。groupPrev.next = pre
: 将前一个组的尾部 (groupPrev
) 连接到当前反转后组的新头部 (pre
)。groupPrev = preTail
: 为下一轮循环做准备,将groupPrev
指针移动到刚刚处理完的组的尾节点上。
- 算法进入一个主
-
返回结果:
- 循环结束后,所有满足条件的组都已被反转并正确链接。
dummy.next
指向了整个新链表的头节点,将其返回。
- 循环结束后,所有满足条件的组都已被反转并正确链接。
完整代码
/*** Definition for singly-linked list.*/
class ListNode {int val;ListNode next;ListNode() {}ListNode(int val) { this.val = val; }ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}class Solution {/*** K 个一组翻转链表。* @param head 原始链表的头节点* @param k 每组的节点数* @return 翻转后链表的头节点*/public ListNode reverseKGroup(ListNode head, int k) {// 步骤 1: 预计算链表的总长度 nint n = 0;ListNode cur = head;while (null != cur) {n++;cur = cur.next;}// 创建哨兵节点,简化头部操作ListNode dummy = new ListNode(0, head);// groupPrev 指向当前待反转组的前一个节点ListNode groupPrev = dummy;// pre 用于组内反转,将成为反转后组的新头ListNode pre = null;// cur 指向组内当前正在处理的节点cur = head;// 主循环:只要剩余节点数足够 k 个,就处理一个组while (n >= k) {// 更新剩余节点数n -= k;// 步骤 2: 核心反转逻辑,反转 k 个节点for (int i = 0; i < k; i++) {ListNode nxt = cur.next; // 暂存下一个节点cur.next = pre; // 当前节点指向前一个节点pre = cur; // pre 前移cur = nxt; // cur 前移}// 步骤 3: 重新链接反转后的组// preTail 是反转前该组的头节点,反转后它变成了尾节点ListNode preTail = groupPrev.next;// 将反转后组的尾部 (preTail) 连接到下一组的头部 (cur)preTail.next = cur;// 将前一个组的尾部 (groupPrev) 连接到反转后组的新头部 (pre)groupPrev.next = pre;// 为下一次循环做准备:更新 groupPrev,它现在是刚刚处理完的组的尾节点groupPrev = preTail;}return dummy.next;}
}
时空复杂度
时间复杂度:O(N)
- 计算长度:第一个
while
循环遍历整个链表一次,时间复杂度为 O(N)。 - 分组反转:主
while
循环和内部的for
循环结合起来,确保了每个节点只会被访问和操作常数次。while
循环执行N/k
次,for
循环执行k
次,总操作数与(N/k) * k = N
成正比。因此,这部分的时间复杂度也是 O(N)。
综合分析:
算法的总时间复杂度是 O(N) + O(N) = O(N)。它对链表进行了两次线性扫描(一次计数,一次反转)。因此,最终时间复杂度为 O(N)。
空间复杂度:O(1)
- 主要存储开销:算法只使用了
dummy
,groupPrev
,pre
,cur
,nxt
,preTail
等固定数量的指针变量。 - 空间大小:这些变量的数量不随输入链表的长度
N
或分组大小k
的变化而改变。
综合分析:
算法没有使用任何与输入规模成比例的额外数据结构。因此,其额外辅助空间复杂度为 O(1)。这是一个高效的原地算法。
参考灵神