链表(典型算法思想)—— OJ例题算法解析思路
目录
一、2. 两数相加 - 力扣(LeetCode)
算法代码:
1. 初始化
2. 遍历链表并相加
3. 返回结果
举例说明
二、24. 两两交换链表中的节点 - 力扣(LeetCode)
算法代码:
代码思路
举例说明
初始状态
第一次交换
第二次交换
最终结果
三、143. 重排链表 - 力扣(LeetCode)
算法代码:
代码思路
举例说明
初始状态
1. 找到中间节点
2. 逆序后半部分链表
3. 合并两个链表
最终结果
代码总结
执行步骤总结
四、23. 合并 K 个升序链表 - 力扣(LeetCode)
解法一:算法代码(利用堆)
代码思路
举例说明
初始状态
执行过程
最终结果
代码总结
执行步骤总结
解法二:算法代码(递归/分治)
代码分析
1. 类的结构
2. 主函数 - mergeKLists
3. 分治法 - merge
4. 平分数组与递归
5. 合并两个有序链表 - mergeTowList
6. 合并过程
7. 合并逻辑
8. 处理剩余节点
举例说明
合并过程
五、25. K 个一组翻转链表 - 力扣(LeetCode)
算法代码:
代码分析
1. 链表节点结构定义
2. 主函数 - reverseKGroup
3. 逆序过程
4. 处理不需要翻转的部分
举例说明
处理过程
总结
一、2. 两数相加 - 力扣(LeetCode)
算法代码:
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x), next(next) {}* };*/
class Solution {
public:ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {ListNode *cur1 = l1, *cur2 = l2;ListNode* newhead = new ListNode(0); // 创建⼀个虚拟头结点,记录最终结果ListNode* prev = newhead; // 尾指针int t = 0; // 记录进位while (cur1 || cur2 || t) {// 先加上第⼀个链表if (cur1) {t += cur1->val;cur1 = cur1->next;}// 加上第⼆个链表if (cur2) {t += cur2->val;cur2 = cur2->next;}prev->next = new ListNode(t % 10);prev = prev->next;t /= 10;}prev = newhead->next;delete newhead;return prev;}
};
1. 初始化
-
创建两个指针
cur1
和cur2
,分别指向两个链表的头节点l1
和l2
。 -
创建一个虚拟头节点
newhead
,用于记录最终的结果链表。这个虚拟头节点的作用是简化链表操作,避免处理头节点的特殊情况。 -
创建一个尾指针
prev
,指向当前结果链表的最后一个节点。 -
初始化一个变量
t
,用于记录进位。
2. 遍历链表并相加
-
使用
while
循环遍历两个链表,直到两个链表都遍历完且没有进位为止。 -
在循环中:
-
如果
cur1
不为空,将cur1
的值加到t
上,并将cur1
指向下一个节点。 -
如果
cur2
不为空,将cur2
的值加到t
上,并将cur2
指向下一个节点。 -
将
t % 10
的值作为新节点的值,并将其添加到结果链表的末尾。 -
更新
prev
指针,使其指向新添加的节点。 -
更新
t
为t / 10
,即计算进位。
-
3. 返回结果
-
循环结束后,结果链表的头节点是
newhead->next
。 -
删除虚拟头节点
newhead
,避免内存泄漏。 -
返回结果链表的头节点。
举例说明
假设有两个链表 l1
和 l2
,分别表示数字 342
和 465
,即:
-
l1
: 2 -> 4 -> 3 -
l2
: 5 -> 6 -> 4
按照代码的逻辑,相加过程如下:
-
初始化:
-
cur1
指向2
,cur2
指向5
。 -
newhead
是一个虚拟头节点,prev
指向newhead
。 -
t = 0
。
-
-
第一次循环:
-
t = 0 + 2 + 5 = 7
。 -
创建新节点
7
,prev->next
指向7
,prev
指向7
。 -
t = 7 / 10 = 0
。 -
cur1
指向4
,cur2
指向6
。
-
-
第二次循环:
-
t = 0 + 4 + 6 = 10
。 -
创建新节点
0
,prev->next
指向0
,prev
指向0
。 -
t = 10 / 10 = 1
。 -
cur1
指向3
,cur2
指向4
。
-
-
第三次循环:
-
t = 1 + 3 + 4 = 8
。 -
创建新节点
8
,prev->next
指向8
,prev
指向8
。 -
t = 8 / 10 = 0
。 -
cur1
和cur2
都为nullptr
,循环结束。
-
-
返回结果:
-
结果链表为
7 -> 0 -> 8
,表示数字807
,即342 + 465 = 807
。
-
二、24. 两两交换链表中的节点 - 力扣(LeetCode)
算法代码:
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x), next(next) {}* };*/
class Solution {
public:ListNode* swapPairs(ListNode* head) {if (head == nullptr || head->next == nullptr)return head;ListNode* newHead = new ListNode(0);newHead->next = head;ListNode *prev = newHead, *cur = prev->next, *next = cur->next,*nnext = next->next;while (cur && next) {// 交换结点prev->next = next;next->next = cur;cur->next = nnext;// 修改指针prev = cur; // 注意顺序cur = nnext;if (cur)next = cur->next;if (next)nnext = next->next;}cur = newHead->next;delete newHead;return cur;}
};
代码思路
-
检查边界条件:
-
如果链表为空(
head == nullptr
)或只有一个节点(head->next == nullptr
),直接返回head
,因为不需要交换。
-
-
创建虚拟头节点:
-
创建一个虚拟头节点
newHead
,并将其next
指向链表的头节点head
。虚拟头节点的作用是简化头节点的交换操作,避免特殊处理。
-
-
初始化指针:
-
prev
:指向当前节点对的前一个节点(初始为newHead
)。 -
cur
:指向当前节点对中的第一个节点(初始为head
)。 -
next
:指向当前节点对中的第二个节点(初始为cur->next
)。 -
nnext
:指向next
的下一个节点(初始为next->next
)。
-
-
遍历链表并交换节点:
-
使用
while
循环遍历链表,直到cur
或next
为空。 -
在循环中:
-
交换节点:
-
将
prev->next
指向next
。 -
将
next->next
指向cur
。 -
将
cur->next
指向nnext
。
-
-
更新指针:
-
将
prev
指向cur
(当前节点对中的第一个节点)。 -
将
cur
指向nnext
。 -
如果
cur
不为空,将next
指向cur->next
。 -
如果
next
不为空,将nnext
指向next->next
。
-
-
-
-
返回结果:
-
将
cur
指向newHead->next
(交换后的链表头节点)。 -
删除虚拟头节点
newHead
,释放内存。 -
返回
cur
(交换后的链表)。
-
举例说明
假设链表为 1 -> 2 -> 3 -> 4
,交换过程如下:
初始状态
-
newHead -> 1 -> 2 -> 3 -> 4
-
prev = newHead
-
cur = 1
-
next = 2
-
nnext = 3
第一次交换
-
交换节点:
-
prev->next
指向2
。 -
next->next
指向1
。 -
cur->next
指向3
。
-
-
更新指针:
-
prev = 1
。 -
cur = 3
。 -
next = 4
。 -
nnext = nullptr
(因为next->next
为空)。
-
第二次交换
-
交换节点:
-
prev->next
指向4
。 -
next->next
指向3
。 -
cur->next
指向nullptr
。
-
-
更新指针:
-
prev = 3
。 -
cur = nullptr
(因为nnext
为空)。 -
循环结束。
-
最终结果
-
交换后的链表为
2 -> 1 -> 4 -> 3
。 -
删除虚拟头节点
newHead
。 -
返回
2
作为链表的头节点。
三、143. 重排链表 - 力扣(LeetCode)
算法代码:
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x), next(next) {}* };*/
class Solution {
public:void reorderList(ListNode* head) {// 处理边界情况if (head == nullptr || head->next == nullptr ||head->next->next == nullptr)return;// 1. 找到链表的中间节点 - 快慢双指针(⼀定要画图考虑 slow// 的落点在哪⾥)ListNode *slow = head, *fast = head;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;}// 2. 把 slow 后⾯的部分给逆序 - 头插法ListNode* head2 = new ListNode(0);ListNode* cur = slow->next;slow->next = nullptr; // 注意把两个链表给断开while (cur) {ListNode* next = cur->next;cur->next = head2->next;head2->next = cur;cur = next;}// 3. 合并两个链表 - 双指针ListNode* ret = new ListNode(0);ListNode* prev = ret;ListNode *cur1 = head, *cur2 = head2->next;while (cur1) {// 先放第⼀个链表prev->next = cur1;cur1 = cur1->next;prev = prev->next;// 再放第⼆个链表if (cur2) {prev->next = cur2;prev = prev->next;cur2 = cur2->next;}}delete head2;delete ret;}
};
代码思路
-
处理边界情况:
-
如果链表为空(
head == nullptr
)、只有一个节点(head->next == nullptr
)或只有两个节点(head->next->next == nullptr
),直接返回,因为不需要重排。
-
-
找到链表的中间节点:
-
使用 快慢双指针:
-
slow
指针每次移动一步。 -
fast
指针每次移动两步。
-
-
当
fast
到达链表末尾时,slow
指向链表的中间节点。 -
将链表从中间节点断开,分为前半部分和后半部分。
-
-
逆序后半部分链表:
-
使用 头插法 将后半部分链表逆序:
-
创建一个虚拟头节点
head2
。 -
遍历后半部分链表,将每个节点插入到
head2
的后面。
-
-
最终,
head2->next
指向逆序后的后半部分链表。
-
-
合并两个链表:
-
使用 双指针 将前半部分链表和逆序后的后半部分链表合并:
-
cur1
指向前半部分链表的头节点。 -
cur2
指向逆序后的后半部分链表的头节点。 -
依次交替连接两个链表的节点。
-
-
最终,链表被重排为所需顺序。
-
-
释放临时节点:
-
删除虚拟头节点
head2
和ret
,释放内存。
-
举例说明
假设链表为 1 -> 2 -> 3 -> 4 -> 5
,重排过程如下:
初始状态
-
链表:
1 -> 2 -> 3 -> 4 -> 5
。
1. 找到中间节点
-
slow
指向3
,fast
指向5
。 -
将链表从
3
断开:-
前半部分:
1 -> 2 -> 3
。 -
后半部分:
4 -> 5
。
-
2. 逆序后半部分链表
-
逆序后的后半部分链表:
5 -> 4
。
3. 合并两个链表
-
交替连接节点:
-
1 -> 5 -> 2 -> 4 -> 3
。
-
最终结果
-
重排后的链表为
1 -> 5 -> 2 -> 4 -> 3
。
代码总结
-
快慢双指针:
-
用于找到链表的中间节点,确保链表被正确分为两部分。
-
-
头插法逆序链表:
-
将后半部分链表逆序,便于后续合并。
-
-
双指针合并链表:
-
交替连接两个链表的节点,完成重排。
-
-
边界条件处理:
-
确保代码在链表节点数较少时也能正常运行。
-
执行步骤总结
-
找到中间节点并断开链表。
-
逆序后半部分链表。
-
合并前半部分和逆序后的后半部分链表。
-
释放临时节点,返回重排后的链表。
四、23. 合并 K 个升序链表 - 力扣(LeetCode)
解法一:算法代码(利用堆)
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x), next(next) {}* };*/
class Solution {struct cmp {bool operator()(const ListNode* l1, const ListNode* l2) {return l1->val > l2->val;}};public:ListNode* mergeKLists(vector<ListNode*>& lists) {// 创建⼀个⼩根堆priority_queue<ListNode*, vector<ListNode*>, cmp> heap;// 让所有的头结点进⼊⼩根堆for (auto l : lists)if (l)heap.push(l);// 合并 k 个有序链表ListNode* ret = new ListNode(0);ListNode* prev = ret;while (!heap.empty()) {ListNode* t = heap.top();heap.pop();prev->next = t;prev = t;if (t->next)heap.push(t->next);}prev = ret->next;delete ret;return prev;}
};
代码思路
-
定义小根堆的比较器:
-
使用
struct cmp
定义一个小根堆的比较器,确保堆顶始终是最小的节点。
-
-
初始化小根堆:
-
创建一个优先队列
heap
,用于存储所有链表的当前节点。 -
将所有链表的头节点加入小根堆(如果链表不为空)。
-
-
合并 K 个有序链表:
-
创建一个虚拟头节点
ret
,用于简化结果链表的构建。 -
使用指针
prev
指向结果链表的最后一个节点。 -
循环从堆中取出最小的节点
t
,并将其连接到结果链表中。 -
如果
t->next
不为空,将t->next
加入小根堆。 -
重复上述步骤,直到小根堆为空。
-
-
返回结果链表:
-
返回
ret->next
,即合并后的有序链表的头节点。 -
删除虚拟头节点
ret
,释放内存。
-
举例说明
假设有以下 3 个有序链表:
-
链表 1:
1 -> 4 -> 5
-
链表 2:
1 -> 3 -> 4
-
链表 3:
2 -> 6
初始状态
-
小根堆中包含所有链表的头节点:
[1, 1, 2]
。
执行过程
-
第一次循环:
-
取出堆顶节点
1
(来自链表 1 或链表 2)。 -
将
1
连接到结果链表。 -
将
1->next
(4
或3
)加入小根堆。 -
堆状态:
[1, 2, 3]
或[1, 2, 4]
。
-
-
第二次循环:
-
取出堆顶节点
1
(来自另一个链表)。 -
将
1
连接到结果链表。 -
将
1->next
(3
或4
)加入小根堆。 -
堆状态:
[2, 3, 4]
。
-
-
第三次循环:
-
取出堆顶节点
2
(来自链表 3)。 -
将
2
连接到结果链表。 -
将
2->next
(6
)加入小根堆。 -
堆状态:
[3, 4, 6]
。
-
-
第四次循环:
-
取出堆顶节点
3
(来自链表 2)。 -
将
3
连接到结果链表。 -
将
3->next
(4
)加入小根堆。 -
堆状态:
[4, 4, 6]
。
-
-
第五次循环:
-
取出堆顶节点
4
(来自链表 1 或链表 2)。 -
将
4
连接到结果链表。 -
将
4->next
(5
或nullptr
)加入小根堆(如果存在)。 -
堆状态:
[4, 5, 6]
。
-
-
第六次循环:
-
取出堆顶节点
4
(来自另一个链表)。 -
将
4
连接到结果链表。 -
将
4->next
(nullptr
)不加入小根堆。 -
堆状态:
[5, 6]
。
-
-
第七次循环:
-
取出堆顶节点
5
(来自链表 1)。 -
将
5
连接到结果链表。 -
将
5->next
(nullptr
)不加入小根堆。 -
堆状态:
[6]
。
-
-
第八次循环:
-
取出堆顶节点
6
(来自链表 3)。 -
将
6
连接到结果链表。 -
堆为空,循环结束。
-
最终结果
-
合并后的链表为:
1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6
。
代码总结
-
小根堆的作用:
-
小根堆用于高效地找到当前最小的节点,确保每次都能将最小的节点连接到结果链表中。
-
-
虚拟头节点的使用:
-
虚拟头节点
ret
简化了结果链表的构建过程。
-
-
时间复杂度:
-
假设 K 是链表的数量,N 是所有链表的总节点数。
-
每个节点被插入和弹出小根堆一次,时间复杂度为
O(N log K)
。
-
-
空间复杂度:
-
小根堆最多存储 K 个节点,空间复杂度为
O(K)
。
-
执行步骤总结
-
将所有链表的头节点加入小根堆。
-
循环从小根堆中取出最小节点,连接到结果链表。
-
将取出的节点的下一个节点加入小根堆。
-
重复上述步骤,直到小根堆为空。
-
返回合并后的有序链表。
解法二:算法代码(递归/分治)
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x), next(next) {}* };*/
class Solution {
public:ListNode* mergeKLists(vector<ListNode*>& lists) {return merge(lists, 0, lists.size() - 1);}ListNode* merge(vector<ListNode*>& lists, int left, int right) {if (left > right)return nullptr;if (left == right)return lists[left];// 1. 平分数组int mid = left + right >> 1;// [left, mid] [mid + 1, right]// 2. 递归处理左右区间ListNode* l1 = merge(lists, left, mid);ListNode* l2 = merge(lists, mid + 1, right);// 3. 合并两个有序链表return mergeTowList(l1, l2);}ListNode* mergeTowList(ListNode* l1, ListNode* l2) {if (l1 == nullptr)return l2;if (l2 == nullptr)return l1;// 合并两个有序链表ListNode head;ListNode *cur1 = l1, *cur2 = l2, *prev = &head;head.next = nullptr;while (cur1 && cur2) {if (cur1->val <= cur2->val) {prev = prev->next = cur1;cur1 = cur1->next;} else {prev = prev->next = cur2;cur2 = cur2->next;}}if (cur1)prev->next = cur1;if (cur2)prev->next = cur2;return head.next;}
};
代码分析
1. 类的结构
// 链表节点结构
struct ListNode {int val; // 节点值ListNode *next; // 指向下一个节点的指针ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), next(nullptr) {}ListNode(int x, ListNode *next) : val(x), next(next) {}
};
-
ListNode
是单链表节点的定义,包含一个整数值val
和指向下一个节点的指针next
。
2. 主函数 - mergeKLists
ListNode* mergeKLists(vector<ListNode*>& lists) {return merge(lists, 0, lists.size() - 1);
}
-
mergeKLists
函数接受一个链表数组lists
,调用merge
函数来合并这些链表,传入左边界和右边界(0 和lists.size() - 1
)。
3. 分治法 - merge
ListNode* merge(vector<ListNode*>& lists, int left, int right) {if (left > right)return nullptr; // 边界情况if (left == right)return lists[left]; // 只有一个链表时返回该链表
-
merge
函数通过递归将链表数组分成两半,继续合并,直到每个子数组只包含一个链表。 -
如果
left
大于right
,则返回nullptr
;如果left
等于right
,则返回对应的链表。
4. 平分数组与递归
int mid = left + right >> 1; // 平分数组
ListNode* l1 = merge(lists, left, mid); // 合并左半部分
ListNode* l2 = merge(lists, mid + 1, right); // 合并右半部分
-
使用索引
mid
将链表数组平分,分别递归调用merge
函数处理左半部分和右半部分。
5. 合并两个有序链表 - mergeTowList
return mergeTowList(l1, l2);
-
在左右两部分排序完成后,调用
mergeTowList
函数合并这两个有序链表。
6. 合并过程
ListNode* mergeTowList(ListNode* l1, ListNode* l2) {if (l1 == nullptr)return l2; // 如果 l1 为空,返回 l2if (l2 == nullptr)return l1; // 如果 l2 为空,返回 l1
-
mergeTowList
函数用于合并两个单独的有序链表。 -
首先处理边界情况,如果某个链表为空,直接返回另一个链表。
7. 合并逻辑
ListNode head;
ListNode *cur1 = l1, *cur2 = l2, *prev = &head;
head.next = nullptr;
while (cur1 && cur2) {if (cur1->val <= cur2->val) {prev = prev->next = cur1; // 将较小的节点加入合并链表cur1 = cur1->next;} else {prev = prev->next = cur2;cur2 = cur2->next;}
}
-
使用两个指针
cur1
和cur2
遍历链表l1
和l2
,通过比较值将较小的节点添加到新的合并链表中,并移动相应的指针。 -
prev
指向合并后的链表的最后一个节点,初始时指向一个虚拟的头节点head
。
8. 处理剩余节点
if (cur1)prev->next = cur1; // 如果 l1 有剩余,连接到合并链表
if (cur2)prev->next = cur2; // 如果 l2 有剩余,连接到合并链表
return head.next; // 返回合并后的链表
-
如果
cur1
或cur2
中还有未遍历的节点,将其连接到合并链表的末尾。 -
返回
head.next
,即合并后链表的头节点。
举例说明
假设我们有三个链表如下:
-
链表 1: 1 -> 4 -> 5
-
链表 2: 1 -> 3 -> 4
-
链表 3: 2 -> 6
合并过程
-
初始调用:
mergeKLists([{1, 4, 5}, {1, 3, 4}, {2, 6}])
-
调用
merge
,此时left = 0
,right = 2
,进行分割。
-
-
第一次分割:
-
中间索引
mid = 1
。分为[0, 1]
和[2]
。 -
对
[0, 1]
继续分割。
-
-
第二次分割:
-
对
[0, 1]
,中间索引mid = 0
,分为[0]
和[1]
。 -
递归返回链表 1 和链表 2。
-
-
合并链表 1 和 链表 2:
-
合并结果为: 1 -> 1 -> 3 -> 4 -> 4 -> 5。
-
-
合并结果与链表 3:
-
最后调用
mergeTowList
合并结果与链表 3。 -
合并后的最终结果为: 1 -> 1 -> 2 -> 3 -> 4 -> 4 -> 5 -> 6。
-
五、25. K 个一组翻转链表 - 力扣(LeetCode)
算法代码:
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x), next(next) {}* };*/
class Solution {
public:ListNode* reverseKGroup(ListNode* head, int k) {// 1. 先求出需要逆序多少组int n = 0;ListNode* cur = head;while (cur) {cur = cur->next;n++;}n /= k;// 2. 重复 n 次:⻓度为 k 的链表的逆序即可ListNode* newHead = new ListNode(0);ListNode* prev = newHead;cur = head;for (int i = 0; i < n; i++) {ListNode* tmp = cur;for (int j = 0; j < k; j++) {ListNode* next = cur->next;cur->next = prev->next;prev->next = cur;cur = next;}prev = tmp;}// 把不需要翻转的接上prev->next = cur;cur = newHead->next;delete newHead;return cur;}
};
代码分析
1. 链表节点结构定义
struct ListNode {int val; // 节点值ListNode *next; // 指向下一个节点的指针ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), next(nullptr) {}ListNode(int x, ListNode *next) : val(x), next(next) {}
};
-
ListNode
是单链表节点的定义,包含一个整数值val
和指向下一个节点的指针next
。
2. 主函数 - reverseKGroup
ListNode* reverseKGroup(ListNode* head, int k) {// 1. 先求出需要逆序多少组int n = 0;ListNode* cur = head;while (cur) {cur = cur->next;n++;}n /= k; // 需要逆序的完整组数
-
该函数接受链表头节点
head
和一个整数 ( k ) 作为输入。 -
首先,计算链表的长度 ( n ),并通过
n /= k
计算需要逆序的完整组数。
3. 逆序过程
ListNode* newHead = new ListNode(0); // 创建一个虚拟头节点
ListNode* prev = newHead; // prev 指向新链表的尾部
cur = head; // cur 指向原链表的头
for (int i = 0; i < n; i++) {ListNode* tmp = cur; // 暂存当前组的头节点for (int j = 0; j < k; j++) {ListNode* next = cur->next; // 暂存下一个节点cur->next = prev->next; // 将当前节点插入到 prev 后面prev->next = cur; // 更新 prevcur = next; // 移动到下一个节点}prev = tmp; // 更新 prev 为当前组的头节点
}
-
创建一个虚拟头节点
newHead
,方便处理链表的插入和返回。 -
使用
prev
指针来构建新的链表,初始时指向newHead
。 -
通过一个外层循环遍历需要逆序的组,每次逆序 ( k ) 个节点。
-
内层循环负责逆序当前 ( k ) 个节点。具体步骤为:
-
暂存当前节点的下一个节点
next
。 -
将当前节点
cur
插入到prev
后面,使得cur
成为逆序链表的一部分。 -
更新
cur
指向下一节点。
-
4. 处理不需要翻转的部分
// 把不需要翻转的接上
prev->next = cur;
cur = newHead->next; // 更新 cur 为新的头节点
delete newHead; // 删除虚拟头节点
return cur; // 返回新的头节点
-
当所有需要翻转的组都处理完后,将不需要翻转的部分直接连接到
prev
的后面。 -
更新
cur
为新链表的头节点,删除虚拟头节点newHead
,并返回链表的新头节点。
举例说明
假设有一个链表如下:
-
输入链表: 1 -> 2 -> 3 -> 4 -> 5
-
k = 2
处理过程
-
计算链表长度:
-
链表长度 ( n = 5 ),计算得到 ( n / k = 2 )。因此,需要逆序 2 组。
-
-
第一次逆序:
-
当前节点
cur
指向 1,暂存为tmp
。 -
逆序 2 个节点:
-
1 -> 2 当前为: 2 -> 1 -> (后续)
-
2 -> 3 更新: 1 -> 2 -> (后续)
-
逆序后链表: 2 -> 1 (指向 3)
-
-
-
第二次逆序:
-
cur
现在指向 3,暂存为tmp
。 -
逆序 2 个节点:
-
3 -> 4 当前为: 4 -> 3 -> (后续)
-
3 -> 5 更新: 4 -> 3 -> (后续)
-
逆序后链表: 4 -> 3 (指向 5)
-
-
-
处理剩余节点:
-
此时
cur
指向 5,prev
指向 3,将 5 直接连接到最后: -
最终链表为:2 -> 1 -> 4 -> 3 -> 5
-
总结
-
最终输出的链表是:2 -> 1 -> 4 -> 3 -> 5。
-
当链表的节点数不足 ( k ) 时(例如以下情况1 -> 2 -> 3 -> 4 -> 5 -> 6,当 ( k = 4 ) 时),只会逆序前 4 个节点,后面的节点保持原顺序,结果为:4 -> 3 -> 2 -> 1 -> 5 -> 6。
相关文章:

链表(典型算法思想)—— OJ例题算法解析思路
目录 一、2. 两数相加 - 力扣(LeetCode) 算法代码: 1. 初始化 2. 遍历链表并相加 3. 返回结果 举例说明 二、24. 两两交换链表中的节点 - 力扣(LeetCode) 算法代码: 代码思路 举例说明 初始状…...

【C++指南】解锁C++ STL:从入门到进阶的技术之旅
💓 博客主页:倔强的石头的CSDN主页 📝Gitee主页:倔强的石头的gitee主页 ⏩ 文章专栏:《C指南》 期待您的关注 目录 一、STL 是什么 二、STL 的核心组件 2.1 容器(Containers) 2.2 算法&…...
LeetCode刷题---字符串---859
亲密字符串 859. 亲密字符串 - 力扣(LeetCode) 题目: 给你两个字符串 s 和 goal ,只要我们可以通过交换 s 中的两个字母得到与 goal 相等的结果,就返回 true ;否则返回 false 。 交换字母的定义是&…...

数据处理中多线程功能的设计逻辑,及python的多线程实现
数据处理中多线程功能的设计逻辑主要是通过并发编程模型来提高程序的执行效率和响应速度。多线程允许在同一进程中创建多个线程,每个线程独立执行任务,同时共享进程的资源(如内存空间)。这种机制特别适用于I/O密集型任务ÿ…...
DeepSeek-R1技术革命:用强化学习重塑大语言模型的推理能力
引言:低成本高性能的AI新范式 在2025年1月,中国AI公司DeepSeek发布了两个标志性模型——DeepSeek-R1-Zero与DeepSeek-R1,以仅600万美元的训练成本实现了与OpenAI O1系列(开发成本约5亿美元)相当的推理性能,…...
python中的深度学习框架TensorFlow 和 PyTorch 有什么区别?
TensorFlow 和 PyTorch 是目前最流行的两个深度学习框架,它们在设计理念、使用方式和社区支持等方面存在一些显著的区别。以下是它们的主要区别: 1. 设计理念 TensorFlow: 静态计算图:TensorFlow 使用静态计算图,即在运行模型之前需要先定义整个计算图。这使得 TensorFlo…...

用 Python 实现 DeepSeek R1 本地化部署
DeepSeek R1 以其出色的表现脱颖而出,不少朋友想将其本地化部署,网上基于 ollama 的部署方式有很多,但今天我要带你领略一种全新的方法 —— 使用 Python 实现 DeepSeek R1 本地化部署,让你轻松掌握,打造属于自己的 AI…...
Spreadjs与GcExcel
GcExcel VS SpreadJS 前言 报表系统前端化,释放后端压力,调高前端研发产能,但随着报表系统的数据量的增加,浏览器的限制,前端报表已达到瓶颈,用户使用体验逐渐不友好,为解决这一问题,找到新的解决方案,所以写下此篇对比 两者分别是什么? SpreadJS 是一款基于 HTML5…...
vue中使用lodash的debounce(防抖函数)
1、安装 npm i --save lodash.debounce2、引入 import debounce from lodash.debounce3、使用 <van-search v-model"searchValue" placeholder"输入姓名或工号" inputhandleInput />第一种: handleInput: debounce(function (val) {c…...
什么是耐环境环形光源
耐环境环形光源是一种专为工业视觉系统设计的光源,能够在恶劣环境下稳定工作。以下是其主要特点和应用: 特点 耐用性:外壳坚固,通常采用金属或高强度塑料,能承受冲击、振动和温度变化。 防护等级:具备高IP防…...

3dtiles——Cesium ion for Autodesk Revit Add-In插件
一、说明: Cesium已经支持3dtiles的模型格式转换; 可以从Cesium官方Aesset中上传gltf等格式文件转换为3dtiles; 也可以下载插件(例如revit-cesium插件)转换并自动上传到Cesium官方Aseet中。 Revit转3dtiles插件使用…...

Edge浏览器清理主页
我们都知道,Microsoft Edge浏览器是微软创造的搜索浏览器,Windows10、11自带。但是你可以看到,每次你打开Edge浏览器的时候都可以看到许多的广告,如图: 导致打开Edge浏览器的时候会遭受卡顿,广告骚扰&#…...
leetcode刷题第十天——栈与队列Ⅱ
本次刷题顺序是按照卡尔的代码随想录中给出的顺序 1047. 删除字符串中的所有相邻重复项 char* removeDuplicates(char* s) {int len strlen(s);char* tmp malloc(sizeof(char) * (len 1));int top -1, idx 0;while(idx < len) {if(top -1) tmp[top] s[idx];else {i…...
硬修复(hPPR)与软修复(sPPR)
什么是PPR? PPR(Post Package Repair)是一种内存修复技术,主要用于修复DRAM(包括LPDDR4、DDR4等)中的存储单元故障。它通过在封装后对内存芯片进行修复,提高良率和可靠性,减少因制造缺陷导致的内存失效。 想象一下,你买了一袋苹果,有些苹果表面可能有个小斑点或者磕…...
filebeat抓取nginx日志
目录 一、抓取普通的应用输出日志到elasticsearch 二、抓取nginx日志输出到ElasticSearch 2.1、nginx.conf设定日志输出为JSON格式 2.2、nginx.conf设定日志按天输出文件 2.3、抓取Nginx JSON到ElasticSearch配置 一、抓取普通的应用输出日志到elasticsearch - type: log…...

TLQ-CN10.0.2.0 (TongLINK/Q-CN 集群)部署指引 (by lqw)
文章目录 安装准备虚拟机部署部署zk集群安装zk集群启动zk集群初始化元数据(zk)关闭zk集群 部署BookKeeper集群安装BookKeeper集群初始化元数据(bk)启动BookKeeper停止 BookKeeper 部署Brokers集群安装Brokers集群启动 broker停止 …...

第 14 天:UE5 C++ 与蓝图(Blueprint)交互!
🎯 目标: ✅ 了解 C 与蓝图(Blueprint)交互的方式 ✅ 在 C 中调用蓝图函数 ✅ 让蓝图访问 C 变量和方法 ✅ 使用 UFUNCTION、UPROPERTY 进行蓝图暴露 ✅ 提高开发效率,让 C 和蓝图开发者高效协作 1️⃣ 为什么要让 C…...
小初高各学科教材,PDF电子版下载
链接:https://pan.quark.cn/s/7c2125f648e2 小初高中电子课本资料pdf合集 高中各科教材 (部分举例) - 语文:新人教版、旧人教版、苏教版等 - 数学:人教A版、沪教版、鄂教版等 - 英语:重大版、人教版…...
Trader Joe‘s EDI 需求分析
Trader Joes成立于1967年,总部位于美国加利福尼亚州,是一家独特的零售商,专注于提供高质量且价格合理的食品。公司经营范围涵盖了各类杂货、冷冻食品、健康食品以及独特的本地特色商品。 EDI需求分析 电子数据交换(EDIÿ…...
python class详解
在 Python 中,class 是用来创建新的数据类型,即对象的蓝图。类可以包含属性(变量)和方法(函数),它们定义了对象的状态和行为。以下是 Python 类的基本概念和用法的详细解释: 定义类…...

wordpress后台更新后 前端没变化的解决方法
使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…...
Python|GIF 解析与构建(5):手搓截屏和帧率控制
目录 Python|GIF 解析与构建(5):手搓截屏和帧率控制 一、引言 二、技术实现:手搓截屏模块 2.1 核心原理 2.2 代码解析:ScreenshotData类 2.2.1 截图函数:capture_screen 三、技术实现&…...
vscode里如何用git
打开vs终端执行如下: 1 初始化 Git 仓库(如果尚未初始化) git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...

以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
一、属性动画概述NETX 作用:实现组件通用属性的渐变过渡效果,提升用户体验。支持属性:width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项: 布局类属性(如宽高)变化时&#…...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
Cesium1.95中高性能加载1500个点
一、基本方式: 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...
服务器硬防的应用场景都有哪些?
服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式,避免服务器受到各种恶意攻击和网络威胁,那么,服务器硬防通常都会应用在哪些场景当中呢? 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...

Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...