目录
前言
例题
1.移除元素
暴力解法
双指针法
2.长度最小的数组
思路(滑动窗口)
3.反转链表
思路
4.删除倒数第n个结点
思路
5.环形链表
思路
总结
一、双指针法的定义:所谓双指针法,就是利用两个指针(不一定是真指针,能储存相应元素的位置就行)分别标识两个位置,然后通过指针所指元素的性质对数组(或者链表结点)进行修改,同时移动指针完成目标的方法。
二、双指针法的四种使用方法:
三、暴力算法和双指针算法有什么区别:
- 暴力算法每次在第二层遍历的时候,是会重新开始(第二层循环的 j 会回溯的初始位置),然后再遍历下去。
- 双指针算法:由于某种单调性,每次再第二层遍历的时候,不需要回溯到初始位置(单调性),而是在满足要求的位置继续走下去或者更新掉。
多说无益,我们接下来用五个具体的例子来展示如何运用双指针法。
class Solution {
public:
int removeElement(vector& nums, int val) {
int size=nums.size();
for(i=0;i
通过一个快指针和一个慢指针在一个for循环内完成两个for循环的工作。
移除元素的过程如图所示:
class Solution {
public:
int removeElement(vector& nums, int val) {
int slowindex=0;
for(int fastindex=0;i
力扣https://leetcode.cn/problems/minimum-size-subarray-sum/
本题需要运用一种双指针法的特殊用法——滑动窗口。
所谓滑动窗口,就是不断的调节子数组的起始位置和终止位置,从而得出我们想要的结果。以题目描述中的情况为例,查找的过程如图所示:
class Solution {
public:
int minSubArrayLen(int target, vector& nums) {
int result=INT32_MAX;
int sum=0; //滑动窗口的数值之和
int i=0; //滑动窗口的起始位置
int length=0; //滑动窗口的长度
for(int j=0;i=s)
{
length=j-i+1;
result=result
力扣https://leetcode.cn/problems/reverse-linked-list/
如果定义一个新的链表实现链表元素的反转,则是对内存空间的浪费。其实只需要改变链表next指针的指向,直接将链表反转即可,不需要重新定义一个新的链表,如图所示:
之前链表的头结点是元素1,反转之后头结点就是元素5,这里并没有添加或者删除结点,而只是改变了next指针的方向。
接下来我们看看如何利用双指针法来实现链表的反转。
代码如下:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *temp; //保存cur的下一个结点
ListNode *cur=head;
ListNode *pre=NULL;
while(cur)
{ //保存cur的下一个结点,因为接下来要改变cur->next的指向了
temp=cur->next;
cur->next=pre; //反转操作
//更新pre和cur指针
pre=cur;
cur=temp;
}
return pre;
}
};
力扣https://leetcode.cn/problems/remove-nth-node-from-end-of-list/
本题是双指针用法的经典应用,如果要删除倒数第n个结点,则让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾,删除slow所指向的结点就可以了。
删除链表中的倒数第n个结点分为如下几个步骤:
代码如下:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if(head->next==NULL)
return NULL;
ListNode *dummyhead=new ListNode(0); //创建虚拟头结点
dummyhead->next=head;
ListNode *fast=dummyhead;
ListNode *slow=dummyhead;
while(n--&&fast!=nullptr)
{
fast=fast->next;
}
//fast再向前移动一步,因为需要让slow指向删除结点的上一个结点
fast=fast->next;
while(fast!=NULL)
{ //fast和slow指针同时移动
fast=fast->next;
slow=slow->next;
}
//删除slow的下一个结点,并返回原头结点
slow->next=slow->next->next;
head=dummyhead->next;
return head;
}
};
力扣https://leetcode.cn/problems/linked-list-cycle-ii/
本题的关键就在于两点:
如何判断链表是否有环?
可以利用快慢指针,分别定义fast和slow指针,从头结点出发,fast指针每次移动两个结点,slow指针每次移动一个结点,如果fast和slow指针在途中相遇,则说明这个链表有环。
如何寻找环的入口?
这里需要运用一些数学知识,假设头结点到环的入口结点的结点数为x,环的入口结点到fast指针和slow指针相遇结点的结点数为y,从相遇结点到环的入口结点的结点数为z,如图所示:
当fast和slow指针相遇时,slow指针移动的节点数为x+y,fast指针移动的结点数为x+y+n(y+z),n的含义为fast指针在环内移动了n圈才遇到slow指针,y+z为一圈内的结点数。
由于fast是一次移动两步,而slow指针是一次移动一步,所以fast指针移动的结点数=2×slow指针移动的结点数,即(x+y)×2=x+y+n(y+z)。
因为要找环的入口,所以计算的是x。最后整理的公式为:x=(n-1)(y+z)+z
如何来理解这个公式呢?
当n=1的时候,x=z。这就意味着一个指针从头结点出发,另一个指针从相遇结点出发,这两个指针每次一起移动一个结点,那么这两个指针相遇的结点就是环入口结点。
其实当n>1的情况也是一样的,无非是从相遇处出发的指针多转了n-1圈,然后再遇到从头结点出发的指针。
代码如下:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *fast=head;
ListNode *slow=head;
while(fast!=NULL&&fast->next!=NULL)
{
slow=slow->next;
fast=fast->next->next;
if(fast==slow)
{
ListNode *index1=fast;
ListNode *index2=head;
//快慢指针相遇
while(index1!=index2)
{ //从头结点和相遇点同时开始查找直至相遇
index2=index2->next;
index1=index1->next;
}
return index2; //返回环的入口
}
}
return NULL;
}
};
双指针法相对于数组,在处理单链表方面的问题更具有优势。因为,要处理单链表中的某个结点,只能通过从头遍历的方式来处理,要想处理指针所指的前一个结点,只能再次从头遍历,这往往会增加算法的时间复杂度,如果利用双指针同时进行相应的移动操作,那么可以大大提高算法的效率。
总之,双指针法的精髓就在于可以时刻进行更新操作,例如滑动窗口以及反转链表的操作。