【C++双指针法】5道题带你认识双指针法

目录

前言

例题

1.移除元素

暴力解法

双指针法

2.长度最小的数组

 思路(滑动窗口)

3.反转链表

思路

 4.删除倒数第n个结点

思路 

 5.环形链表

 思路

 总结


 

前言

一、双指针法的定义:所谓双指针法,就是利用两个指针(不一定是真指针,能储存相应元素的位置就行)分别标识两个位置,然后通过指针所指元素的性质对数组(或者链表结点)进行修改,同时移动指针完成目标的方法。

二、双指针法的四种使用方法:

  1. 用快指针指向符合条件的数据填充慢指针指向的位置;
  2. 两个指针指向的元素相互比较,选出符合条件 的元素填入指定容器;
  3. 解决一遍遍历解决不了但两遍或多次遍历能解决的问题;
  4. 通过快慢指针的移动以及两者的相遇选出的特定条件;

三、暴力算法和双指针算法有什么区别:

  • 暴力算法每次在第二层遍历的时候,是会重新开始(第二层循环的 j 会回溯的初始位置),然后再遍历下去。
  • 双指针算法:由于某种单调性,每次再第二层遍历的时候,不需要回溯到初始位置(单调性),而是在满足要求的位置继续走下去或者更新掉。

多说无益,我们接下来用五个具体的例子来展示如何运用双指针法。

例题

1.移除元素

 
力扣https://leetcode.cn/problems/remove-element/

暴力解法

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
    	
    	int size=nums.size();
    	for(i=0;i<size;i++)
    	{
    		if(nums[i]==val)
    		{
    			for(j=i+1;j<size;j++)
    			{
    				nums[j-1]=nums[j]; //数组不能像链表一样删除元素,只能进行覆盖 
				}
				i--;  //因为下标i以后的数值都向前移了一位,所以i也要向前移动一位
				size--; //删除了一个元素,此时数组的长度-1 
			}
		}
		return size; 

    }
};
  •  时间复杂度:O(n)。
  • 空间复杂度:O(1)。

双指针法

通过一个快指针和一个慢指针在一个for循环内完成两个for循环的工作。

移除元素的过程如图所示:

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
    	
    	int slowindex=0;
		for(int fastindex=0;i<nums.size();fastindex++)
		{
			if(val!=nums[fastindex])  //当快指针指向被删元素时,if内语句不执行,快指针比慢指针
			{                         //快一步则用快指针指向的元素覆盖慢指针指向的元素。 
				nums[slowindex++]=nums[fastindex];
			}
		}
		
		return slowindex; 

    }
};
  • 时间复杂度:O(n)。
  • 空间复杂度:O(1)。

2.长度最小的数组

力扣https://leetcode.cn/problems/minimum-size-subarray-sum/

 思路(滑动窗口)

本题需要运用一种双指针法的特殊用法——滑动窗口。

所谓滑动窗口,就是不断的调节子数组的起始位置和终止位置,从而得出我们想要的结果。以题目描述中的情况为例,查找的过程如图所示:

  • 窗口内的元素:保持窗口内数值总和大于或等于s的长度最小的连续子数组;
  • 移动窗口的起始位置:如果当前窗口的值大于s。则窗口向前移动(也就是窗口该缩小了);
  • 移动窗口的结束位置:窗口的结束位置就是for循环遍历数组 的指针; 
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
           
           int result=INT32_MAX;
           int sum=0;  //滑动窗口的数值之和 
           int i=0;    //滑动窗口的起始位置 
           int length=0;  //滑动窗口的长度 
           for(int j=0;i<nums.size();j++)
           {
           	  sum+=nums[j]; 
           	  //注意这里使用while,每次更新i以及最小的长度 
           	  while(sum>=s)
				{
					length=j-i+1;
					result=result<length?result:length;
					//这就是滑动窗口的精髓之处,不断变更i(子数组起始位置) 
					sum-=nums[i++];
				} 
		   }
		   //如果result没有被赋值,说明无符合条件的子数组 
		   return result==INT32_MAX?0:result;
    }
};
  •  时间复杂度:O(n)。
  • 空间复杂度:O(1)。

3.反转链表

力扣https://leetcode.cn/problems/reverse-linked-list/

思路

如果定义一个新的链表实现链表元素的反转,则是对内存空间的浪费。其实只需要改变链表next指针的指向,直接将链表反转即可,不需要重新定义一个新的链表,如图所示:

之前链表的头结点是元素1,反转之后头结点就是元素5,这里并没有添加或者删除结点,而只是改变了next指针的方向。

接下来我们看看如何利用双指针法来实现链表的反转。

  • 首先定义一个cur指针,指向头结点,再定义一个pre指针,指向NULL;
  • 首先使用temp指针保存cur->next结点,原因是反转操作会改变cur以及pre的指向,temp可以储存下一次反转操作cur指向的位置;
  • 反转操作:cur->next指向pre;
  • 更新cur和pre结点:pre=cur,cur=temp;
  • 然后循环执行以上逻辑,最后cur指向NULL,循环结束,链表也反转完毕。此时pre指向的就是反转之后的新头结点; 

 

代码如下:

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;
        
    }
};

 4.删除倒数第n个结点

力扣https://leetcode.cn/problems/remove-nth-node-from-end-of-list/

思路 

本题是双指针用法的经典应用,如果要删除倒数第n个结点,则让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾,删除slow所指向的结点就可以了。

删除链表中的倒数第n个结点分为如下几个步骤:

  1. 推荐使用虚拟头结点,这样方便处理删除实际头结点的逻辑;
  2. 定义fast指针和slow指针,初始值为虚拟头结点(dummyhead),删除导数第n个结点;
  3. fast先向前移动n+1步,原因是只有这样,fast和slow同时移动的时候slow才能指向删除结点的上一个结点(方便做删除操作);
  4. fast和slow同时移动,直至fast指向末尾;
  5. 删除slow指向的下一个结点; 

代码如下:

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;

    }
};

 5.环形链表

 力扣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;
        
    }
};

 总结

双指针法相对于数组,在处理单链表方面的问题更具有优势。因为,要处理单链表中的某个结点,只能通过从头遍历的方式来处理,要想处理指针所指的前一个结点,只能再次从头遍历,这往往会增加算法的时间复杂度,如果利用双指针同时进行相应的移动操作,那么可以大大提高算法的效率。

总之,双指针法的精髓就在于可以时刻进行更新操作,例如滑动窗口以及反转链表的操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值