文章目录
因为是篇个人向的文章,所以博主还是设置了一下阅读门槛,不好意思了哈.
虚拟头结点 哨兵结点
在链表的题目中,虚拟头结点常常被使用,也被称为哨兵结点。哨兵节点,其实就是一个附加在原链表最前面用来简化边界条件的附加节点,它的值域不存储任何东西,只是为了操作方便而引入。虚拟头结点常常被命名为dummyhead
.
例如原链表为1->2->3
,引入哨兵结点后就变为dummyhead->1->2->3
,后续处理完链表的相关操作后,通常返回的值是dummyhead->next
,当然也是看题目的要求哈.
ListNode* dummyhead = new ListNode(0, head);
双指针法 快慢指针
双指针法,顾名思义,通常是指定义两个指针,其中一个指针所处位置相对在前(链表的深处),另一个指针则相对靠后,前者习惯命名为fast、cur、first
,后者习惯命名为slow、pre、second
.
该方法通常被用于避免多次遍历整个链表,有时候当我们用一个指针无法解决链表问题时,可以尝试使用多个指针 ~~
真题:leetcode 剑指offer22 链表倒数第k个节点 双指针法 代码鲁棒性
解题链接
剑指 Offer 22. 链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
思路:使用fast指针先往前走K步,然后fast与slow指针同时出发,当fast指针走到头的时候,slow指针就是我们想要的结果。
AC代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
if(head == NULL || k==0) return NULL; //前两种情况特判
//一次遍历
ListNode* first = head;
ListNode* second = head;
for(int i=0; i<k;i++){
if(first != NULL) first = first->next;
else return NULL;
}
while(first!=NULL){
first = first->next;
second = second->next;
}
return second;
}
};
举一反三:求链表的中间结点也可以使用该方法,fast结点每次走两步,slow结点每次走一步。
真题:删除链表的倒数第N个结点 双指针法 链表 虚拟头结点
解题链接
思路:双指针法,一遍扫描
AC代码
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
//双指针法
//因为可能删除了头结点,所以新引入了虚拟头结点
ListNode* res = new ListNode(0, head);
ListNode* del_node = res;
for(int i=0;i<n;i++) head = head->next;
while(head!=nullptr){//head一直往前走,直到到达空结点
head = head->next;
del_node = del_node->next;
}
//由于del_node初始从虚拟结点出发,此时正位于要删除的结点前一个位置
//删除要求的结点
del_node->next = del_node->next->next;
return res->next;
}
};
练习题目
LeetCode160 相交链表
LeetCode21 合并两个有序链表
LeetCode876 链表的中间结点
四结点法
我眼中的四结点法:通常在需要分段处理链表时使用,因为前一段与后一段在分别处理完成之后,前一段的尾结点要与后一段的头结点链接起来,所以常常会用到四个结点,分别是:
待处理的这段链表的头结点与尾结点(start、end)
、这段链表头结点的上一个结点(虚拟头结点dummyhead)
、这段链表尾结点的下一个结点(虚拟尾结点dummytail)
。
想一想,这段链表尾结点的下一个结点是不是就是下一段要处理的链表的首结点?
这段链表头结点的上一个结点是不是就是上一段链表的尾结点?这样就能够正确处理好段与段之间的链接关系
。
小细节:第一段待处理链表的虚拟头结点通常是前哨结点;最后一段待处理链表的虚拟尾结点通常是Null
专题:链表反转
真题:LeetCode 206. 反转链表 辅助结点
解题链接
像这一道题,其实可以视为四结点法的简化版本。该链表需要相邻的两个结点依次反转,也即需要一个虚拟尾结点或临时结点来保存后续的指向关系,而虚拟头结点则无需使用:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
//依次处理 每次仅需要处理一对结点的连接关系
//注意这里是为了将初始的头结点的next置空,很重要 也可以理解为引入了前哨结点
ListNode* pre = nullptr;
ListNode* cur = head; //此时pre 与 cur就构成了一对待处理的结点
ListNode* tmp; //辅助结点 保留下一个待处理的结点
while(cur!=nullptr){//我愿称呼这四行代码为链表反转四件套
tmp = cur->next;
cur->next = pre;
//下一对要处理的结点
pre = cur;
cur = tmp;
}
return pre;
}
};
真题:LeetCode 92. 反转链表II
题目链接
链表反转的进阶版,像这里就要注意处理这一段链表反转之后与上一段以及下一段链表之间的链接关系
AC代码
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
if(left == right) return head;
//有可能从头开始翻转 引入头结点
ListNode* dummyhead = new ListNode(0, head);
ListNode* start = dummyhead; // start是待翻转那段链表的虚拟头结点
ListNode* pre = dummyhead->next;
ListNode* cur = pre->next;
for(int i=1;i<right;i++){
if(i<left){
start = start -> next;
pre = pre -> next;
cur = cur->next;
}
else{//又见四件套
ListNode* tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
}
start->next->next = cur; //处理虚拟尾结点的连接关系
start->next = pre; //处理虚拟头结点的连接关系
return dummyhead->next;
}
};
真题:LeetCode 24. 两两交换链表中的节点
题目链接
每次处理两个相邻结点的链接关系,然后往后移动两个位置,得到新的待处理结点对,依次处理。
每一对待处理的结点都可以视为待处理的一段结点,这时候四点法的作用就体现出来了:
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if(head == nullptr || head->next == nullptr) return head;
ListNode* res = new ListNode(0, head);
ListNode* dummyhead = res;
while(dummyhead->next!=nullptr && dummyhead->next->next != nullptr){ // 至少有一对待处理结点
ListNode* pre = dummyhead->next;
ListNode* cur = dummyhead->next->next;
ListNode* dummytail = cur->next;
cur->next = pre;
//处理虚拟头结点与虚拟尾结点的关系
pre -> next = dummytail;
dummyhead -> next = cur;
dummyhead = pre;
}
return res->next;
}
};
真题:LeetCode 25 K个一组翻转链表 分组+链表反转 虚拟头、尾结点
解题链接
解题思路:
确实不愧为hard难度哈。
整体思路是先每K个结点分为一组,然后结合链表翻转的代码对该分组进行翻转。需要注意的是该组翻转之后,头、尾结点要分别与上一组的尾结点以及下一组的头结点关联起来,形成一个完整的链表。
为了达到该目的,给该组的待翻转链表加上:
- 上一组的尾结点作为虚拟头结点
- 下一组的头结点作为虚拟尾结点
具体过程结合代码注释即可进行分析。
class Solution {
public:
ListNode* reversek(ListNode* start, ListNode* end){
ListNode* pre = start->next;
ListNode* cur = start->next->next;
ListNode* tmp;
while(cur != end){//链表翻转四件套
tmp = cur->next;
cur->next = pre;
//更新
pre = cur;
cur = tmp;
}
//更新虚拟头结点与虚拟尾结点的连接关系
//虚拟头结点 start 翻转之后的链表头结点 pre
//虚拟尾结点 end 翻转之后的链表尾结点 start->next
//记录翻转之后的链表尾结点
ListNode* res = start->next;
start->next->next = end; //关联尾部
start->next = pre; //关联头部
return res; //返回翻转之后的链表尾结点 也即下一组待翻转链表的虚拟头结点
}
ListNode* reverseKGroup(ListNode* head, int k) {
if(k == 1) return head;
ListNode* dummyhead = new ListNode(0, head); //前哨结点
ListNode* pre = dummyhead; //虚拟头结点
ListNode* cur = head;//当前结点
int cnt = 1;
while(cur!=nullptr){
if(cnt%k!=0){
cur = cur -> next;//新增一个结点
}
else{
//翻转该组链表 同时注意更新pre
//注意传入的一组链表不仅包含了K个结点,还包含了虚拟头尾结点
//虚拟尾结点可由cur指针进行更新,因此下面的函数需要返回新的虚拟头结点
pre = reversek(pre, cur->next); //返回下一组链表的dummyhead
cur = pre->next; // 更新cur指针
}
cnt +=1;
}
return dummyhead->next;
}
};
综合题目:LeetCode143. 重排链表
解题思路:
寻找链表中点+反转链表+合并链表
class Solution {
public:
void reorderList(ListNode* head) {
ListNode* dummyhead = new ListNode(0, head);
ListNode* fast = head;
ListNode* slow = head;
//寻找链表中点
//使得slow结点指向中节点 或者双中节点时的左节点
while(fast->next!=nullptr && fast->next->next!=nullptr){
fast = fast->next->next;
slow = slow->next;
}
下一个结点才是真正要反转的头结点
ListNode* cur = slow->next;
//翻转后一段链表
ListNode* pre = nullptr;
while(cur!=nullptr){ //四件套
ListNode* tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
//dummy->next 与pre 两段链表合并
cur = dummyhead->next;
while(pre!=nullptr){//后一段一定不会长于前一段
ListNode* tmp1 = cur->next;
ListNode* tmp2 = pre->next;
cur->next = pre;
pre->next = tmp1;
cur = tmp1;
pre = tmp2;
}
cur->next = nullptr;
head = dummyhead->next;
}
};