题目描述
给你链表的头节点 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) 额外内存空间的算法解决此问题吗?
原题:LeetCode 25
思路及实现
方式一:递归
思路
其大致过程可以分解为
- 找到待翻转的k个节点(注意:若剩余数量小于 k 的话,则不需要反转,因此直接返回待翻转部分的头结点即可)。
- 对其进行翻转。并返回翻转后的头结点(注意:翻转为左闭又开区间,所以本轮操作的尾结点其实就是下一轮操作的头结点)。
- 对下一轮 k 个节点也进行翻转操作。
- 将上一轮翻转后的尾结点指向下一轮翻转后的头节点,即将每一轮翻转的k的节点连接起来。
示意图
代码实现
Java 版本
/** * Definition for singly-linked list. * public 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) { if (head == null || head.next == null) { return head; } ListNode tail = head; for (int i = 0; i < k; i++) { // 如果剩余数量小于 k,则不需要反转。 if (tail == null) { return head; } tail = tail.next; } // 反转前 k 个元素 ListNode newHead = reverse(head, tail); // 下一轮的开始的地方就是 tail head.next = reverseKGroup(tail, k); return newHead; } /** * 反转链表中左闭右开区间的节点 * @param head 左闭区间的头节点 * @param tail 右开区间的尾节点 * @return 反转后的链表的头节点 */ private ListNode reverse(ListNode head, ListNode tail) { ListNode pre = null; ListNode next = null; while (head != tail) { next = head.next; head.next = pre; pre = head; head = next; } return pre; } }
说明:
reverseKGroup() 方法用于将链表每个大小为 k 的子列表进行反转。
如果输入的头节点 head 或其下一个节点为空,则无需翻转,直接返回头节点。
使用 tail 指针找到当前子列表的结束节点(即当前子列表的下一组的开始节点)。
如果剩余节点数量不足 k,则无需进行翻转,直接返回头节点。
调用 reverse() 方法反转当前子列表,并得到翻转后的新的头节点 newHead。
通过递归调用 reverseKGroup() 方法,将下一轮的开始位置 tail 作为参数传入。
将当前子列表的头节点 head 的 next 指针指向下一轮的结果,连接翻转后的下一组子列表。
返回翻转后的新的头节点 newHead。
C 语言版本
struct ListNode { int val; struct ListNode *next; }; /** * 反转以头节点head开始,尾节点为tail前一个节点的链表 * 返回反转后的链表的头节点 */ struct ListNode* reverse(struct ListNode* head, struct ListNode* tail) { struct ListNode* prev = NULL; struct ListNode* curr = head; struct ListNode* next = NULL; while (curr != tail) { next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; } /** * 反转每个大小为k的连续节点子链表 * 返回修改后的链表的头节点 */ struct ListNode* reverseKGroup(struct ListNode* head, int k) { if (head == NULL || head->next == NULL) { return head; } struct ListNode* tail = head; for (int i = 0; i < k; i++) { // 如果剩余节点数不足k个,无需反转,直接返回头节点 if (tail == NULL) { return head; } tail = tail->next; } // 反转前k个节点 struct ListNode* newHead = reverse(head, tail); // 递归调用反转后续的子链表,并将结果连接到当前子链表的末尾 head->next = reverseKGroup(tail, k); return newHead; }
说明:
结构体 ListNode 定义了链表节点的结构,包含一个整型变量 val 和一个指向下一个节点的指针 next。
reverse() 函数用于反转以 head 节点为开始,以 tail 节点为前一个节点的子链表,并返回反转后的链表的头节点。
在 reverse() 函数中,使用三个指针 prev、curr 和 next 分别表示前一个节点、当前节点和下一个节点。
在 while 循环中,将当前节点 curr 的 next 指针指向前一个节点 prev,实现反转。
通过更新指针的位置,进行下一次的节点遍历。
返回反转后的链表的头节点 prev。
reverseKGroup() 函数用于反转每个大小为 k 的连续节点的子链表,并返回修改后的链表的头节点。
在 reverseKGroup() 函数中,如果输入的头节点 head 或其下一个节点为空,则无需反转,直接返回头节点。
使用指针 tail 找到当前子链表的结束节点(即当前子链表的下一组的开始节点)。
如果剩余节点数量不足 k,则无需反转,直接返回头节点。
调用 reverse() 函数反转当前子链表,并得到反转后的新头节点 newHead。
递归调用 reverseKGroup() 函数,对剩余的子链表进行反转,并将结果连接到当前子链表的末尾。
返回反转后的新头节点 newHead。
Python3 版本
class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse(head: ListNode, tail: ListNode) -> ListNode: """ 反转以head为头节点、tail为尾节点的子链表 并返回反转后的链表的头节点 """ prev = None curr = head while curr != tail: next = curr.next curr.next = prev prev = curr curr = next return prev def reverseKGroup(head: ListNode, k: int) -> ListNode: """ 反转每个大小为k的连续子链表 并返回修改后的链表的头节点 """ if not head or not head.next: return head tail = head # 找到子链表的尾节点 for _ in range(k): if not tail: return head tail = tail.next # 反转子链表,并获取反转后的新的头节点 new_head = reverse(head, tail) # 递归调用,将下一组子链表的头节点连接到当前子链表的末尾 head.next = reverseKGroup(tail, k) return new_head
代码说明:
ListNode 类定义了链表节点的结构,包含一个整数类型变量 val 和一个指向下一个节点的指针 next。
reverse() 函数用于反转以 head 节点为开始,以 tail 节点为前一个节点的子链表,并返回反转后的链表的头节点。
在 reverse() 函数中,使用三个指针 prev、curr 和 next 分别表示前一个节点、当前节点和下一个节点。
在 while 循环中,将当前节点 curr 的 next 指针指向前一个节点 prev,实现反转。
通过更新指针的位置,进行下一次的节点遍历。
返回反转后的链表的头节点 prev。
reverseKGroup() 函数用于反转每个大小为 k 的连续节点的子链表,并返回修改后的链表的头节点。
在 reverseKGroup() 函数中,如果输入的头节点 head 或其下一个节点为空,则无需反转,直接返回头节点。
使用指针 tail 找到当前子链表的结束节点(即当前子链表的下一组的开始节点)。
如果剩余节点数量不足 k,则无需反转,直接返回头节点。
调用 reverse() 函数反转当前子链表,并得到反转后的新头节点 new_head。
递归调用 reverseKGroup() 函数,对剩余的子链表进行反转,并将结果连接到当前子链表的末尾。
返回反转后的新头节点 new_head。
复杂度分析
- 时间复杂度:O(n),其中 n 是链表的长度。每个节点都被遍历一次,每次遍历反转 k 个节点。
- 空间复杂度:O(n/k),递归调用栈的深度
方式二:迭代和原地反转
思路
迭代和原地反转的方法是通过遍历链表,对每个子链表进行原地反转,然后将反转后的子链表拼接到最终结果中。
代码实现
Java 版本
class Solution { /** * 反转以 head 为头节点的链表中的前 k 个节点 * 返回反转后的头节点以及反转后的尾节点 */ private ListNode reverseK(ListNode head, int k) { ListNode prev = null; ListNode curr = head; for (int i = 0; i < k; i++) { ListNode next = curr.next; curr.next = prev; prev = curr; curr = next; } return new ListNode[]{prev, head}; } public ListNode reverseKGroup(ListNode head, int k) { ListNode dummy = new ListNode(0); // 创建一个虚拟头节点 dummy.next = head; ListNode prev = dummy; // prev 始终指向每个子链表的反转前的最后一个节点 ListNode curr = head; // curr 用于遍历链表 while (curr != null) { ListNode tail = curr; // tail 保存每个子链表的最后一个节点 int count = 0; // count 记录当前子链表的长度 while (curr != null && count < k) { curr = curr.next; count++; } if (count < k) { // 如果剩余节点数不足 k,则不需要反转,直接跳出循环 break; } ListNode[] result = reverseK(tail, k); // 反转当前子链表的前 k 个节点 ListNode reversedHead = result[0]; // 反转后的头节点 ListNode reversedTail = result[1]; // 反转后的尾节点 // 将反转后的子链表接入链表 prev.next = reversedHead; reversedTail.next = curr; prev = reversedTail; // 更新 prev 指针 } return dummy.next; } } •
说明:
reverseK 方法用于反转以 head 为头节点的链表中的前 k 个节点。返回反转后的头节点和尾节点(这里使用了一个数组来返回多个节点)。
reverseKGroup 方法实现以 k 个一组翻转链表的功能。
创建一个虚拟头节点 dummy 来简化链表操作。
prev 指针始终指向每个子链表的反转前的最后一个节点。
curr 指针用于遍历链表。
使用循环遍历链表,直至 curr 为 null,这样可以处理剩余不足 k 个节点的情况。
在循环中,先找到当前子链表的最后一个节点 tail。
然后,再遍历 k 个节点,通过调用 reverseK 方法来反转这个子链表的前 k 个节点。
获取反转后的头节点 reversedHead 和尾节点 reversedTail。
将反转后的子链表接入链表中,即将 prev 的 next 指向 reversedHead,reversedTail 的 next 指向 curr。
更新 prev 指针,使其指向反转后的尾节点。
C 语言版本
struct ListNode { int val; struct ListNode *next; }; /** * 反转以 head 为头节点、tail 为尾节点的子链表 * 并返回反转后的链表的头节点 */ struct ListNode* reverseLinkedList(struct ListNode* head, struct ListNode* tail) { struct ListNode* prev = NULL; struct ListNode* curr = head; while (curr != tail) { struct ListNode* next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; } /** * 按照 k 个一组翻转链表 * 并返回修改后的链表的头节点 */ struct ListNode* reverseKGroup(struct ListNode* head, int k) { struct ListNode* dummy = malloc(sizeof(struct ListNode)); // 创建一个虚拟头节点 dummy->val = 0; dummy->next = head; struct ListNode* prev = dummy; // prev 始终指向每个子链表的反转前的最后一个节点 struct ListNode* curr = head; // curr 用于遍历链表 while (curr != NULL) { struct ListNode* tail = curr; // tail 保存每个子链表的最后一个节点 int count = 0; // count 记录当前子链表的长度 while (curr != NULL && count < k) { curr = curr->next; count++; } if (count < k) { break; // 如果剩余节点数不足 k,则不需要反转,直接跳出循环 } struct ListNode* reversedHead = reverseLinkedList(tail, curr); // 反转当前子链表 struct ListNode* reversedTail = tail; // 将反转后的子链表接入链表 prev->next = reversedHead; reversedTail->next = curr; prev = reversedTail; // 更新 prev 指针 } struct ListNode* newHead = dummy->next; free(dummy); // 释放虚拟头节点的内存 return newHead; } •
说明:
reverseLinkedList 函数用于反转以 head 为头节点,以 tail 为前一个节点的子链表,并返回反转后的链表的头节点。
在 reverseLinkedList 函数中,使用两个指针 prev 和 curr 分别表示前一个节点和当前节点。
在 while 循环中,将当前节点 curr 的 next 指针指向前一个节点 prev,实现反转。
通过更新指针的位置,进行下一次的节点遍历。
返回反转后的链表的头节点 prev。
reverseKGroup 函数实现按照 k 个一组翻转链表的功能。
创建一个虚拟头节点 dummy 并将其指向链表的头部,以便于处理头节点的情况。
使用两个指针 prev 和 curr 分别指向当前子链表的最后一个节点和遍历节点。
在循环中,首先找到当前子链表的末尾节点 tail,然后再遍历 k 个节点。
如果剩余的节点数量不足 k 个,则不需要反转,直接退出循环。
调用 reverseLinkedList 函数反转当前子链表,并获取反转后的头节点 reversedHead 和尾节点 reversedTail。
将反转后的子链表接入链表中,即将 prev 的 next 指针指向 reversedHead,reversedTail 的 next 指针指向 curr。
更新 prev 指针,向后移动到反转后的子链表的尾节点。
返回虚拟头节点 dummy 的 next 指针,即为反转后的链表的头结点。
Python3 版本
class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverseLinkedList(head: ListNode, tail: ListNode) -> ListNode: prev = None curr = head while curr != tail: next_node = curr.next curr.next = prev prev = curr curr = next_node return prev def reverseKGroup(head: ListNode, k: int) -> ListNode: dummy = ListNode(0) # 创建一个虚拟头节点 dummy.next = head prev = dummy # prev 始终指向每个子链表的反转前的最后一个节点 curr = head # curr 用于遍历链表 while curr: tail = curr # tail 保存每个子链表的最后一个节点 count = 0 # count 记录当前子链表的长度 while curr and count < k: curr = curr.next count += 1 if count < k: break # 如果剩余节点数不足 k,则不需要反转,直接跳出循环 reversed_head = reverseLinkedList(tail, curr) # 反转当前子链表 reversed_tail = tail # 将反转后的子链表接入链表 prev.next = reversed_head reversed_tail.next = curr prev = reversed_tail # 更新 prev 指针 return dummy.next
说明:
reverseLinkedList 函数用于反转以 head 为头节点、tail 为尾节点的子链表,并返回反转后的链表的头节点。
在 reverseLinkedList 函数中,使用两个指针 prev 和 curr 分别表示前一个节点和当前节点。
在 while 循环中,将当前节点 curr 的 next 指针指向前一个节点 prev,实现反转。
通过更新指针的位置,进行下一次的节点遍历。
返回反转后的链表的头节点 prev。
reverseKGroup 函数用于按照 k 个一组翻转链表。
创建一个虚拟头节点 dummy 并将其指向链表的头部,以便于处理头节点的情况。
使用两个指针 prev 和 curr 分别指向当前子链表的最后一个节点和遍历节点。
在循环中,首先找到当前子链表的末尾节点 tail,然后再遍历 k 个节点。
如果剩余的节点数量不足 k 个,则不需要反转,直接退出循环。
调用 reverseLinkedList 函数反转子链表,并获取反转后的头节点和尾节点。
将反转后的子链表接入链表中,即将 prev 的 next 指针指向反转后的头节点,尾节点的 next 指针指向下一个子链表的头节点。
更新 prev 指针,向后移动到反转后的子链表的尾节点。
复杂度分析
- 时间复杂度:O(n),其中 n 是链表的长度。每个节点恰好被访问两次:一次是遍历整个链表,一次是进行反转操作。
- 空间复杂度:O(1)。只使用了常数级别的额外空间来进行指针操作,没有使用额外的数据结构。
总结
递归法 | 迭代+原地反转方法 | |
思路 | 将链表划分为大小为k的子链表,递归处理 | 使用循环迭代遍历链表,并在每次迭代中原地反转子链表 |
时间复杂度 | O(n),每个节点被遍历一次 | O(n),每个节点被遍历一次 |
空间复杂度 | O(n/k),递归调用栈的深度 | O(1),原地修改链表 |
(如果递归栈的深度达到n/k,则创建了O(n/k)个递归调用栈帧) | (不需要额外的空间,仅使用常数级别的指针变量和变量存储空间) | |
优点 | 实现简单,逻辑清晰 | 不需要额外的递归调用栈,适用于大规模链表 |
代码可读性好 | 原地修改链表,不需要额外空间 | |
缺点 | 递归调用栈可能溢出 | 实现相对复杂,需要处理指针的连接 |
额外的空间复杂度 | 需要对子链表进行循环遍历和反转 | |
特点 | 可以处理较小规模的链表 | 适用于大规模链表处理和优化空间复杂度 |
可读性好,思考和实现过程接近问题描述 | 可读性相对较差,实现相对复杂 |