目录
前言
双指针一般分为两种,一种是使用两个方向相同的指针进行扫描的快慢指针,另外一种是使用两个方向不同的指针进行扫描的对撞指针。而今天,我们要看的是快慢指针。
一、快慢指针是什么?
快慢指针是一种常见的算法技巧,通常定义出两个指针并以不同的速度遍历链表。一般情况下,慢指针每次移动一个节点,快指针每次移动两个节点。用此方法可以解决一些链表中的问题,并且提高了代码效率,简化了时间复杂度。
二、快慢指针的应用
1.寻找特殊节点
1.简介
- 运用快慢指针遍历链表,当快指针走到最后时,慢指针则指向所要找的特殊位置。
2.经典例题
- 题目
代码如下:
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* slow=head,*fast=head;//快慢指针都指向头节点
while(fast&&fast->next)//当快指针所指向的节点或下一节点为空时,结束循环
{
slow=slow->next;//慢指针每次走一步
fast=fast->next->next;//快指针每次走两步
}
//循环执行完后,慢指针指向所要找的中间节点(特殊位置)
return slow;
}
时间复杂度为O(L),L为链表的长度 。
- 题目
代码如下:
struct ListNode* removeNthFromEnd(struct ListNode* head, int n)
{
struct ListNode*phead=(struct ListNode*)malloc(sizeof(struct ListNode));//创建一个哑节点
phead->val=0;//初始化哑节点
phead->next=head;//初始化哑节点
struct ListNode*n1=phead;//慢指针
struct ListNode*n2=n1->next;//快指针
while(n--)//快指针先走n步,使得快慢指针相差n个距离
{
n2=n2->next;
}
while(n2)//快慢指针一起走,当快指针所指向的节点为空时,慢指针刚好指向要删除节点的前驱节点
{
n1=n1->next;//慢指针走一步
n2=n2->next;//快指针走一步
}
struct ListNode*cur=n1->next;//储存要删除的节点
n1->next=n1->next->next;
free(cur);//释放掉要删除的节点
struct ListNode*ans=phead->next;//储存表头
free(phead);//释放哑节点
return ans;//返回表头
}
时间复杂度为O(L),L为链表的长度 。
注意:假设不创建哑节点,快慢指针都开始都指向表头,其他不变。程序执行完后,慢指针指向要删除的节点,但是为了删除此节点,我们应该找要删除节点的前驱节点,因为前驱节点存有要删除节点的地址,否则如果直接释放掉要删除的节点,会造成野指针的发生。因此,创建一个哑节点,把慢指针往前移一步,则最终会指向要删除节点的前驱节点。
还有另外一种解法:先翻转链表,然后删除要删除的节点,最后再翻转链表。(此方法思路简单,但是代码量多)
代码如下:
struct ListNode* removeNthFromEnd(struct ListNode* head, int n)
{
struct ListNode*cur=head;
struct ListNode*prev=NULL;
struct ListNode*next=NULL;
while(cur)//翻转链表
{
next=cur->next;
cur->next=prev;
prev=cur;
cur=next;
}//最终prev为翻转后新链表的头节点
struct ListNode*phead=(struct ListNode*)malloc(sizeof(struct ListNode));//创建哑节点
phead->val=0;//初始化哑节点
phead->next=prev;//初始化哑节点
struct ListNode*p1=phead;//p1指向哑节点
struct ListNode*p2=prev;//p2指向翻转后链表的头结点
while(--n)//找到要删除链表的前驱节点
{
p1=p1->next;
p2=p2->next;
}
struct ListNode*p=p2;
p1->next=p2->next;
free(p);//释放要删除的节点
struct ListNode*cur1=phead->next;
struct ListNode*prev1=NULL;
struct ListNode*next1=NULL;
while(cur1)//再次翻转链表
{
next1=cur1->next;
cur1->next=prev1;
prev1=cur1;
cur1=next1;
}//最终prev1为翻转后新链表的头节点
free(phead);//释放哑节点
return prev1;
}
2.链表的环形问题
1.简介
- 运用快慢指针遍历链表,利用快指针永远比慢指针快的特性,来判断链表是否有环。如果快慢指针最终相遇,则该链表有环,反之,无环。
2.经典例题
- 题目
代码如下:
bool hasCycle(struct ListNode *head)
{
if(head==NULL||head->next==NULL)//如果该链表没有节点或者只有一个节点,则该链表一定没有环
{
return false;
}
struct ListNode *fast=head;
struct ListNode *slow=head;
while(fast&&fast->next)//当快指针所指向的节点或下一节点为空时,结束循环
{
slow=slow->next;//慢指针每次走一步
fast=fast->next->next;//快指针每次走两步
if(fast==slow)//如果有环,则快指针在某一时刻一定会追上慢指针
{
return true;
}
}
return false;//反之,则无环
}
时间复杂度为O(L),L为链表的长度。
- 题目
- 思路
设头节点到入环口需要走a步,环长为c。
设快慢指针相遇时,慢指针走了b步,则快指针走了2b步。
设快指针比慢指针多走了n圈,则2b-b=nc,b=nc。
慢指针在环中走了b-a=nc-a圈,因此,在相遇的位置再走a圈就走到入环口了。
同时,头节点开始也走a步,也就到入环口了,即二者同时走,最终会在入环口相遇。
代码如下 :
struct ListNode *detectCycle(struct ListNode *head)
{
if(head==NULL||head->next==NULL)
{
return NULL;
}
int i=1;//判断是否有环
struct ListNode *fast=head;
struct ListNode *slow=head;
struct ListNode *cur=NULL;
while(fast&&fast->next)
{
slow=slow->next;//慢指针一次走一步
fast=fast->next->next;//快指针一次走两步
if(fast==slow)//当快指针追上慢指针时,此时快慢指针距离入环口还差a步
{
i=0;
cur=fast;
break;
}
}
if(i)//判断是否有环,如果没有环,则返回空指针
{
return NULL;
}
struct ListNode *ph=head;
while(cur!=ph)//当ph和cur相遇时,即到入环口时,结束循环
{
ph=ph->next;//ph从头节点开始走,走a步就到入环口了
cur=cur->next;//cur从快慢指针相遇开始走,走a步也就到入环口了
}
return ph;
}
时间复杂度为O(L),L链表的长度。
总结
快慢指针在链表这一方面的应用比较多,合理的运用快慢指针,会帮助我们更高效的解决与之相对应的复杂问题,更好的优化我们的程序。
作者第一次在CSDN上写文章,我的基础有限,目前还在学数据结构与算法。文章中难免会有错误的地方,请大家多多包涵,也希望大家能指出错误,作者会一一回应。欢迎大家在评论区里留言,感谢诸位啦~~~