难度:困难
题目:
给你链表的头节点 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个节点的翻转,具体步骤如下:
- 初始化辅助指针和哑节点:创建一个哑节点(dummy node)并指向链表的头部,这样做可以简化边界处理,避免对头节点的特殊判断。同时定义两个指针
pre
和end
,初始时pre
指向哑节点,end
也指向哑节点。 - 遍历链表:使用
end
指针向前移动k个节点,检查是否能够找到k个节点。如果链表剩余部分不足k个节点,则无需再进行翻转,直接返回哑节点的下一个节点(即原链表的头节点)。否则,进行下一步。 - 翻转子链表:确定了要翻转的子链表区间后,使用一个辅助函数(如上面的
reverse
)来实现子链表的反转。反转后,原本的子链表头部将成为新的尾部,尾部将成为新的头部。 - 重连链表:将反转后的子链表重新连接到原链表中。具体来说,将哑节点的
next
指向反转后子链表的新头部,然后将原子链表的尾部(现在是反转后的新头部的下一个节点)与反转后子链表的尾部(原头部)的下一个节点连接起来,以确保链表的连续性。 - 更新指针并继续遍历:将
pre
指针更新为当前子链表翻转后的尾部(即原头部),end
指针重置为pre
的下一个节点,然后继续进行下一轮的k个节点的查找和可能的翻转。 - 循环结束条件:当
end
指针到达链表尾部时,循环结束,最终返回哑节点的下一个节点,即处理后的链表的头节点。
JavaScript实现:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
function reverseKGroup(head, k) {
if (!head || k <= 1) return head;
let dummy = new ListNode(0);
dummy.next = head;
let pre = dummy;
let end = dummy;
while (end.next !== null) {
for (let i = 0; i < k && end !== null; i++) {
end = end.next;
}
if (end === null) break; // 不足k个节点,结束循环
let start = pre.next;
let then = end.next;
end.next = null; // 断开原链表
pre.next = reverse(start); // 反转子链表
start.next = then; // 恢复链接
pre = start; // 移动pre指针
end = pre; // 重置end指针
}
return dummy.next;
}
// 辅助函数,反转链表
function reverse(head) {
let prev = null;
let curr = head;
while (curr !== null) {
let next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
哑节点/哨兵节点
哑节点(Dummy Node),也常被称为哨兵节点(Sentinel Node),是一种特殊的节点,通常被用在链表数据结构的操作中,以简化边界条件的处理。哑节点不存储任何有效数据,其主要作用包括但不限于:
- 简化边界处理:在链表操作中,经常需要处理头节点和尾节点的特殊情况,比如插入、删除操作时需要考虑头结点前是否有节点,或者尾节点后是否有节点。通过在链表的头部或尾部添加一个哑节点,可以使得所有正常节点都有前后指针,从而统一处理逻辑,避免对头尾节点的单独判断。
- 方便链表操作:例如在双端队列或某些链表操作中,哑节点可以作为始终存在的参照点,使得插入和删除操作总是有固定的前驱或后继节点,无需担心空指针异常。
- 优化循环终止条件:在循环遍历链表时,哑节点可以作为一个明确的起始或结束标志,使得循环的终止条件更加简洁明了,避免额外的边界检查。
- 便于链表的初始化:在创建链表时,直接让头指针指向一个哑节点可以简化初始化过程,避免对空链表的特殊处理。
例如,在链表的插入、删除操作中,哑节点可以作为操作的起点,使得插入到链表头部或删除头节点的操作与插入到其他位置或删除其他节点的操作保持一致,无需对头节点进行特殊处理。这样,代码可以更加通用和简洁。