LeetCode24 两两交换链表中的节点
思考:
这题和day3的反转链表的处理方法相同,画个简单的模拟过程图就可以知道,也是需要设置三个辅助指针cur、pre、temp。
在这题中,cur指向一对交换元素中的左边元素(交换前),temp指向右边元素(交换前);pre指向交换后的右边元素,用于控制pre所指结点的next指向下一对交换后元素的左边元素。
见下图:
需要注意的还有只有空链表和奇数个元素的链表,需要特别考虑进入循环的判断条件。其中只有一个元素的链表和奇数个元素的链表同样适用于这个条件:
cur!=nullptr&&cur->next!=nullptr
我的代码:
/**
* 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) {
//设置虚拟头结点方便确定头两个结点交换后头结点的位置
ListNode*newHead=new ListNode(0,head);
//这题还是设置三个辅助结点:cur、pre、temp
ListNode*cur=head,*pre=newHead;
ListNode*temp;
//设置temp指针有两个条件:一个是链表不为空,另一个是链表只有一个元素
if(cur!=nullptr&&cur->next!=nullptr){
temp=cur->next;
// head=temp;
}
//进入循环的条件同上
while(cur!=nullptr&&cur->next!=nullptr){
cur->next=temp->next;
pre->next=temp;
temp->next=cur;
cur=cur->next;
pre=temp->next;
if(cur!=nullptr)
temp=cur->next;
}
//及时释放虚拟头结点内存
head=newHead->next;
delete newHead;
return head;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
LeetCode19 删除链表的倒数第N个节点
思考:有没有可能类似这种涉及“线性表中任意两个元素间的加和/差值/相隔元素个数/操作位于两元素之间的元素”的题,都可以优先考虑双指针法?
这题在刷了前几天的几题双指针类型的题目后,可以很轻松地想到利用快慢指针来确定倒数第n个元素,同时还可以实现一趟遍历就能完成删除操作。
fast:用于从头开始遍历整个链表,直到链表最后一个结点
slow:比fast更晚出发,晚几轮循环由N决定,用于确定待删除结点的前一个结点
我的代码:
/**
* 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* removeNthFromEnd(ListNode* head, int n) {
//用快慢双指针很方便,尝试一次遍历就找到倒数第n个结点
int count=0;
ListNode*newHead=new ListNode(0,head);
ListNode*fast=newHead,*slow=newHead;
ListNode*del=nullptr;
//快慢指针定位要删除的倒数第n个结点的*前一个结点*
while(fast->next!=nullptr){
//slow比fast慢n-1次循环
if(count>n-1){
slow=slow->next;
}
count++;
fast=fast->next;
}
//del是要删除的元素,delete要删除的元素并释放虚拟头结点
del=slow->next;
slow->next=slow->next->next;
delete del;
head=newHead->next;
delete newHead;
return head;
}
};
顺便在这加一个突然想明白的点,是否设置虚拟头结点,要看所求的结果是否是改动过的链表的头结点。
也就是说,涉及到链表的改动操作并要求返回新链表的头结点时,设置虚拟头结点更方便;涉及到查询链表的元素(不改动)时,不用设置虚拟头结点。
- 时间复杂度: O(n)
- 空间复杂度: O(1)
面试题02.07 链表相交
思考:写完以后看力扣评论区,听说这是一道很简单的题,但是题目介绍得很复杂😅我也是读了好几遍题目,磕磕绊绊写完代码以后又磕磕绊绊debug过测试用例,才完全搞明白题目要干什么。
题目描述的是一个Y型的双头链表,两头链表可能会在某个结点相交,相交结点指的是同一ListNode结构,即数值和next指针都相同,而不是相同数值的ListNode。
当两个链表各自的某个结点都指向同一个next,或两个链表本就是同一链表(绕口令?),则那个next或同一链表的head就是要求的相交结点。
可以先分别遍历两个链表确定各自的长度,然后先行开始遍历长链表,将长短链表尾部对齐以后再开始遍历找同一ListNode。
改bug的过程中遇到很多一开始没考虑清楚的边界条件,比如两个空链表、元素相同(相交)的两个单元素链表、元素不相同(不相交)的两个单链表、不相交的长度相同的两个链表......下次刷题要关注到题目中的变量范围提示,在构思的时候就尽可能考虑到多种情况:
我的代码:
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int countA=0,countB=0,diff=0;
ListNode*tempA=headA,*tempB=headB;
//确定两个链表的长度
while(tempA!=NULL){
tempA=tempA->next;
countA++;
}
while(tempB!=NULL){
tempB=tempB->next;
countB++;
}
//两个长度相减得到长度差
if(countA>=countB)
diff=countA-countB;
else
diff=countB-countA;
//设置长短链表各自的虚拟头结点
ListNode* longList=new ListNode(0),*shortList=new ListNode(0);
longList->next=countA>=countB?headA:headB;
shortList->next=countA<countB?headA:headB;
//如果短链表不是空链表或者不是单元素链表时进入循环
while(shortList!=NULL&&shortList->next!=NULL){
//当两链表当前节点的next为同一结点时结束循环
if(longList->next==shortList->next)
break;
if(diff<=0)
shortList=shortList->next;
//将长短链表尾部对齐,先开始进行长链表的遍历
diff--;
longList=longList->next;
}
//增加longList->next==shortList->next条件是用于排除两链表长度相等但是不相交的情况
if(shortList!=NULL&&longList->next==shortList->next&&shortList->next!=NULL)
return shortList->next;
else return NULL;
}
};
时间不太够,回头有空再对比视频和学习网站的解法看看有没有可以优化的地方吧。
LeetCode142 环形链表II
思考:
看到题目以后硬想一个小时毫无头绪的我打开视频题解刷到评论区:
监拆懂?
但是非常感谢卡哥,提供的思路超级绝,初听视频中卡哥讲思路还有点莫名其妙不知所云,自己再拿纸笔复现一遍有种醍醐灌顶的感觉,这题直接秒了。
拆解这题要实现的要求可以知道,需要完成的操作有两步:
1、判断链表是否存在环
2、确定环的入口结点
*判断链表是否存在环
可以很容易想到尝试用双指针——快慢指针法来完成。
当链表不存在环时,快指针一定会先走完整个链表,两指针不会相遇。当存在环时,两指针的速度比例合适的情况下,快慢指针会在环内相遇,而且一定是在环内相遇并且快指针会在慢指针尚未走完一圈的情况下追上慢指针。
假设慢指针一次一个结点,快指针一次两个结点,则快指针所走路程是慢指针的两倍。这样的速度比例还有一个好处,快指针相对于慢指针以每次移动一个结点的速度追赶慢指针,不会出现赶上慢指针但是跳过慢指针而没法相遇的情况。
关于快慢指针相遇时慢指针尚未走完一圈的严谨证明,可以参考代码随想录的学习网站,我就先记住结论做题了^^
*确定环的入口
设头结点到环入口结点的距离为,环入口到两指针相遇结点的距离为
,相遇点到入口的距离为
列个简单的相遇问题等式,由于快指针所走路程是慢指针的两倍,且在两指针相遇时快指针有可能已经转了若干圈,可得原型式
是慢指针所走路程,
是快指针所走路程,k表示快指针转的圈数且一定大于1.
化简可得头结点到环入口结点的距离公式
根据公式的原型式的原理,令k=1得到
,可理解为从相遇点和从头结点到环入口距离相等,故需要确定相遇的结点(可在判断是否存在环的过程中求得),此时再使用两个新指针分别从相遇点和头结点同时出发(在这里k=1可以理解为两个指针的速度比例为1),直到两新指针相遇,相遇的新结点就是环入口。
我的代码:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
//环一定是从链表尾部结点的next再指向链表中的任意结点(呈6型或o型),所以环不可能存在于链表中间
//先判断是否存在环
ListNode*slow=head,*fast=head;
ListNode*meetNode=NULL;
//找快慢指针相遇结点
while(fast!=NULL&&fast->next!=NULL){
fast=fast->next->next;
slow=slow->next;
if(fast==slow){
meetNode=fast;
break;}
}
if(fast==NULL||fast->next==NULL)
return NULL;
//再确定环入口在哪里
else {
//根据公式(x=(k-1)(y+z)+z)推理的原型,需要先确定相遇的结点是哪个
//再通过公式令k=1得到x=z,即从相遇点和从头结点到环入口距离相等
//此时再使用两个指针分别从相遇点和头结点开始出发(在这里k=1可以理解为两个指针的速度倍数为1),直到两指针相遇,相遇的结点就是环入口
ListNode* beginN=head,*meetN=meetNode;
while(beginN!=meetN){
beginN=beginN->next;
meetN=meetN->next;
}
return meetN;
}
}
};
*文章是本人刷题过程中的一些笔记和理解,记录的解析不一定足够清晰,也可能存在本人暂未意识到的错误,如有问题欢迎大家指出。文章中学习到的解法来自代码随想录的B站视频(链表4~6)以及代码随想录的学习网站