双指针算法

注:本文中均用下标指指针。

一.移动0核心算法

遇到0,cur++,des不动,严格划分三个区间。遇到非0,des+1与cur元素互换,然后

des++,cur++

https://leetcode.cn/problems/move-zeroes/submissions/713510468https://leetcode.cn/problems/move-zeroes/submissions/713510468

复制代码
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        for(int cur = 0,des = -1;cur < nums.size();cur++)
        {
            if(nums[cur])
            {
              swap(nums[cur],nums[++des]);//小巧思,既让des+1与cur互换了,也让des++了
            }
        }
    }
};

二.复写0核心算法

https://leetcode.cn/problems/duplicate-zeroshttps://leetcode.cn/problems/duplicate-zeros

算法一.异地操作

因为题目要求,算法一无法使用,我们优化一下,将"异地"改"就地"。

算法二.就地操作

会有des出界的特殊情况,因此判断条件是des >= n - 1,而非==n - 1。

在复写时由于从前往后会把数组元素覆盖掉,因此选择从后往前。

三.快慢指针

https://leetcode.cn/problems/happy-number/submissions/719636739https://leetcode.cn/problems/happy-number/submissions/719636739

1.满足循环链表的条件

在解题前,我们来探讨一下,一个怎样的链表才能被称作循环链表?

此时,先引入这个板块的核心------快慢双指针。定义快指针为fast,慢指针为slow;快指针在链表上一次走两步,慢指针一次走一步;慢指针初始指向链表第一个元素,快指针指向第二个。

先举个跑步的例子:假如两个人在一条跑道上并排同向奔跑,一个快一些,一个慢一些,假如永远跑下去,都不会相遇,那么这条跑道就不是成环封闭的。如果相遇了,就代表快的人到后面套了慢的人一圈,则说明跑道是成环封闭的。

那么回到指针与链表上,两个指针不断在链表上移动,当指针相遇,也就是指向同一位置时,就说明这个链表是循环链表。

那这个时候就会有人问了,假如指针一前一后,永远保持这个状态,那么即便是循环链表,是不是也不会相遇,永远一前一后?虽然一前一后,但快慢两个指针移动速度不同,所以这种想法错了。

2.题解思路

①.大体分析

注意:下面提到的指针并非那个指向什么区域的ptr,我们创建一个int变量,将每次bitsum(实现取位,开平方,并求和的函数) 后的返回值赋值给该变量,就模拟出指针的效果。同样,提到的循环链表也并非实际存在的链表,是一种对数字每一次拆解,开平方,求和后的结果所串起的流程,进行逻辑上的模拟。

根据题设可知,所有情况都是建立在链表是循环的基础之上的,在此之上,分成两种情况:

总结一下就是:

循环体内所有数为1,则满足快乐数;非1则不是。

②.代码实现的角度分析

那么将以上思想转化为代码,该如何考虑呢?

首先定义一个bitsum 函数,用于取位,开平方,并求和。

既然题设情况全是循环链表,但满足条件的链表,循环体元素都为1,我们就创建快慢指针去模拟出的链表上遍历,快指针在链表上一次走两步,慢指针一次走一步;慢指针初始指向链表第一个元素(n),快指针指向第二个(如果快慢指针均为n,也就是均指向首元素的话,是进不了while循环的)。当slow = fast = 1时,就说明是快乐数。

复制代码
class Solution {
public:
     int bitsum(int n)
    {//执行每位求平方,并求和的操作。
      int sum = 0;
      int bit = 0;
      while(n)
      {
          bit = n % 10;
          sum += bit * bit;
          n /= 10;
      }
      return sum;
    }
    bool isHappy(int n) 
    {
        int slow = n,fast = bitsum(n);
        while(slow != fast)
        {//slow挪一步,fast挪两步
            slow = bitsum(slow);
        fast = bitsum(bitsum(fast));//小巧思
        }
        //出循环则双指针相遇,判断下相遇值是否为1,为1就快乐
        return slow == 1;
    }
};

四.盛最多水的容器(根据单调性小巧思解题)

https://leetcode.cn/problems/container-with-most-waterhttps://leetcode.cn/problems/container-with-most-water

1.单调性小巧思

整个集合:

我们截取整个集合的一个区间来研究。

以高度小的那个边为基准,从大的那个边开始,去向内(向小这边 )枚举,高度一直是小高度(不变)或者小于小高度宽度 在向内过程中不断减小 ,那么所得的体积也必然在不断减小。

所以区间内V最大的时候就是V初始的时候 (区间左边界(高为6)和右边界(高为4)围成容器的时候)

2.解题思路

①.大体思路

先从最左和最右边界开始,不断的定区间,同时求区间里的Vmax(左右边界宽度*小的高度,既区间中V的初始值 ),并舍掉区间中高度小(数组中数值小)的那一边。 毕竟区间里不断向小(高度)的那一边去枚举,求V,上面的规律告诉我们,这必定是个V减小的过程,那么V肯定比不过Vmax。既然比不过,干嘛去劳神费力的逐个枚举,干脆在初始时求个Vmax,然后干掉当下区间中高度小的那个边界,停滞枚举。在干掉小(高度)边界以后,继续找新区间,继续求区间的Vmax,继续干当下区间的小(高度)边界,就这么循环往复,求出各个区间的Vmax,再把这些个Vmax拿来比较,看谁是最大的。

②.代码角度分析

Ⅰ.复杂度

根据上面建区间的过程,我们知道需要两个指针去指向左右边界,并移动,所以仍旧要用到双指针。两个指针于两侧不断向中间遍历,在完成分所有区间,去小高度边界后,两个指针才完整的走了数组一遍 ,那么复杂度比起暴力枚举是很低的,才O(N)

Ⅱ.实现思路

数组储存边界高度,规定左右指针谁数值小,谁移动(干小高度边界)。先计算区间最大容积,每计算完一个容积,就去与之前区间的最大容积比较一下,然后更新一下容积,再移动指针。反复求容积,移动,直至俩指针相遇。

注:要记得min,max接口,不然写容积更新的时候着实坐牢,还有求vector的大小要用size()接口,不要sizeof!!!!!!!!

复制代码
class Solution {
public:
    int maxArea(vector<int>& height) {
        int left = 0;
        int right = height.size() - 1;
        int V_max = 0;
        int V = 0;
      while(left < right)
      {
        V = min(height[left],height[right]) * (right - left);
        //更新容积
        V_max = max(V_max,V);
        //移动指针
        if(height[left] <= height[right])
        {
            ++left;
        }
        else
        {
            --right;
        }
      }
      return V_max;
    }
};

五.有效三角形个数

https://leetcode.cn/problems/valid-triangle-numberhttps://leetcode.cn/problems/valid-triangle-number

解法1.暴力枚举

满足三角形的条件不多说了吧:

根据这个来写个伪代码,看看开销:

可以看到,三层循环嵌套,效率也是低到了姥姥家:O(N^3)

让我们优化一下:

解法2.利用单调性,使用双指针

①.排序小巧思

对三边排好序,再判断

这时候边c最大,那么任意一个边加它都必将大于第三条边 ,不管三边关系满不满足三角形,所以下面两个条件不需要去判断,只需判断a+b > c这个条件即可。

②.实现思路

先对整个边长序列进行排序(升序) ,然后挑出最右(最大)的数,固定,作为c。然后开始枚举剩下的a,b两数,left和right指向的数作a,b。当然,枚举仍旧不能胡乱枚举,不然开销又上去了。要将left指向剩下序列最左(最小),right指向最右(最大),此时就又能发现一个规律:

将b固定,a向右枚举,既然整个序列最小值2 + b都能大于c了,那么后面这几个数+b也必然大于c,那么left和right间(包含left)枚举的总次数就等于right - left,就可以用差值代替逐个枚举求总次数,节省开销。 得到这个总次数,9就没有利用价值了,就可以给它干掉,让right--

这时候right指向5,既然2倍的b(b为当下序列最大)都跟c一样大(没法形成三角形),那么a,b间(包括a)这几个比b小的数+b肯定比c小,就是说这一序列的所有枚举情况都没法形成三角形,就没必要一个个枚举,直接干掉2,让left++

总结一下就是:

注:a是指当下区间最左,b是最右

当left与right相遇时,一轮的遍历结束,然后让c--,固定,然后重复以上操作,直至c到整个数组最左。

复制代码
class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        //l.排序
       sort(nums.begin(),nums.end());
       //2.固定+统计枚举次数
       int n = nums.size();
       int ret = 0;
       for(int i = n - 1;i >= 0;i--)
       {
        int left = 0;
        int right = i - 1;
        while(left < right)
        {
          if(nums[left] + nums[right] > nums[i])
          {
            ret += (right - left);
            --right;
          }
          else
          {
            ++left;
          }
        }
       }
       return ret;
    }
};

3.开销对比

六.和为s的两个数字(七的pioneer)

由于题目被力扣删了,所以这题没链接

解法1.暴力枚举

固定一个数,让这个数和数组里剩余的数依次匹配,看是否等于target;枚举完一轮,固定的指针++,然后接着固定,接着向后匹配,反复操作,直至枚举完所有的数。

伪代码:

开销为**O(N^2)。**依旧慢到了姥姥家。

解法2.依旧利用单调性,使用双指针

这里利用了序列升序的特性,在最左(最小)和最右(最大)定义了指针left和right,left指向的数代表固定的数,right指向的数代表当下区间最大的数 ,先让left指向的数去+right指向的数,代表固定数去加当下区间最大数,如果和小于t,那么说明连最大数加固定数都搞不定,就没必要去浪费时间枚举了,就把这个固定数干掉,即left++。假如和>t,就说明当下区间中有枚举值可以满足,但right指向的数与left指向的数之和超过t了,我们需要找一个比right指向的值更小的值去使得和等于t,就把right干掉,即right--。

总结一下:

最坏情况left和right走到序列正中,开销为O(N)

代码:

七.三数之和

https://leetcode.cn/problems/3sumhttps://leetcode.cn/problems/3sum

注意:不包含重复三元组的意义:

解法一.暴力枚举

1.排序

先对整个序列进行升序排序

2.枚举

从左到右依次枚举出满足条件的三元组

3.去重

将重复的几个三元组放进set容器 里,就可以做到去重,详情参见这篇文章一.1节

解法二.对于有序的序列,要想起双指针

注:此处序列换了一个更具代表性的。

1.排序

2.双指针

①.算法讲解

先固定上一个数a,然后在除这个数以外的后面区间内,再找两个数,使得这两个数之和等于-a

在后面区间找两个数之和就是第六题的解法了。

找到所有元组后,进行去重操作,但解法一的去重操作会让你跟offer无缘,因此下面详细讲解更优质的操作。

②.细节把控
Ⅰ.不漏

由于这里的双指针要找到所有满足的元组 ,因此在找到一组以后,不能直接返回,需要找下一组,缩小区间(right++,left--) ,然后在缩小的区间里接着寻找满足条件的元组,但此时需要注意重复的问题。

Ⅱ.更优质的去重

i跳过重复:

++第二个-4所包含的所有情况必然是第一个-4情况的子集,所以可以无需枚举,直接跳过。++

Ⅲ.关于越界

当固定i后,l会一直向后移动,以达到去重,最后越界。

此处结合下面代码实现部分进行讲解。

Ⅳ.关于固定数a的优化

倘若a>0,那么a后面区间里的数必然全部大于0 ,因此后面区间里所有数,都不可能与a相加得0, 所以只需要考虑a<=0的情况 ,所以a <= 0。

3.基于双指针算法的代码实现

复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
      //装输出结果的容器
      vector<vector<int>> ret;
      //1.排序
      sort(nums.begin(),nums.end());
      //2.双指针
      int n = nums.size();
      for(int i = 0; i < n;)//固定a
      {
        //优化a
        if(nums[i] > 0) break;
        //创建双指针
        int left = i + 1, right = n - 1,target = -nums[i];
        while(left < right)
        {//小区间内双指针
          int sum = nums[left] + nums[right];
          if(sum > target) right--;//右边过大,需向左移动
          else if(sum < target) left++;//左边过小,向右移动
          else//满足和为0
          {
           ret.push_back({nums[i],nums[left],nums[right]});
           left++,right--;
           //left,right去重
           while(left < right && nums[left] == nums[left - 1]) left++;
           while(left < right && nums[right] == nums[right + 1]) right--;
          }
        }
        //到此执行完一次双指针算法
        //i的去重与向后遍历
        ++i;
        while(i < n && nums[i - 1] == nums[i]) ++i;
      }
     return ret;
    }
};
处理left和right越界

我们就添加一个限定条件:

++left < right++

因为区间是left到right,left向后移动,出了right就越界;同时right往前移动,也不能越过left。

left和right去重
i去重与防止越界

1.i要确保<n。

2.将for循环里的i++挪到循环内部,不然去重后再进for循环时,会多++一次,导致有个i被落下。

八.四数之和

18. 四数之和 - 力扣(LeetCode)18. 四数之和 - 给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 nums\[a, numsb, numsc, numsd] (若两个四元组元素一一对应,则认为两个四元组重复): * 0 <= a, b, c, d < n * a、b、c 和 d 互不相同 * numsa + numsb + numsc + numsd == target你可以按 任意顺序 返回答案 。 示例 1:输入:nums = 1,0,-1,0,-2,2, target = 0输出:\[-2,-1,1,2,-2,0,0,2,-1,0,0,1]示例 2:输入:nums = 2,2,2,2,2, target = 8输出:\[2,2,2,2] 提示: * 1 <= nums.length <= 200 * -109 <= numsi <= 109 * -109 <= target <= 109https://leetcode.cn/problems/4sum

解法一.暴力枚举

跟第七题的暴力枚举一样,依旧先排序,再暴力枚举,最后将满足条件的元组,装入set去重。

这里会出现四层循环,从而超时,因此还得用解法二双指针进行优化。

解法二.双指针(仍旧排序+双指针)

步骤1.排序

步骤2.双指针

①.算法讲解

可以先依次固定一个a,随后在后面区间里,利用第七题的三数之和,找到三个数,使得和为target - a。 三数之和就可以变成在区间里,再依次固定一个数b,再在区间里再找一个区间,利用第六题二数之和,使得和为 target - a - b。

②.细节把控
Ⅰ.不漏

仍旧找到目标元组后不停止,缩小区间,left++,right--,继续在新区间寻找符合条件的元组。

Ⅱ.不重

要保证三个不重,最内层区间的left和right的不重,次外层区间b的不重,以及最外层区间a的不重。

代码编写所遇问题

此处出现了数据溢出,也就是输入数据过大的问题,需要把数据类型转成longlong。

完整代码

复制代码
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> ret;
        //排序
        sort(nums.begin(),nums.end());
        //nums[i]为a,nums[i + 1]为b
        int n = nums.size();
        for(int i = 0;i < n;)//固定a
        {
            for(int j = i + 1;j < n;)//固定b
            {//双指针
              int left = j + 1,right = n - 1;
             long long sum = (long long)target - (long long)nums[i] - (long long)nums[j];
              while(left < right)
              {
                if(left < right && nums[left] + nums[right] > sum) right--;
                else if(left < right && nums[left] + nums[right] < sum) left++;
                else
                {//找到了符合条件的元组,去重。
                  ret.push_back({nums[i],nums[j],nums[left],nums[right]});
                  left++,right--;
                  //去重left和right
                  while(left < right && nums[left] == nums[left - 1]) left++;
                  while(left < right && nums[right] == nums[right + 1]) right--;
                }
              }
              //去重b(nums[j])
              j++;
              while(j < n && nums[j] == nums[j - 1]) j++;

            }
            //去重a(nums[i])
            i++;
            while(i < n && nums[i] == nums[i - 1]) i++;
        }
        return ret;
    }
};
相关推荐
中微极客1 小时前
剪枝与量化:让YOLO在边缘设备上高效部署
算法·yolo·剪枝
Hesionberger2 小时前
LeetCode406:重建身高队列精髓解析
开发语言·数据结构·python·算法·leetcode
不要葱花2 小时前
接下来我将复现 10 篇强化学习算法:第 3 篇,一杯喜茶,搞定 Search-R1
算法·面试
geovindu3 小时前
CSharp: Recursion Algorithm
开发语言·后端·算法·c#·递归算法
z小猫不吃鱼3 小时前
ResRep: Lossless CNN Pruning via Decoupling Remembering and Forgetting
算法·cnn·剪枝
卡提西亚3 小时前
leetcode-239. 滑动窗口最大值
算法·leetcode·职场和发展
ShallWeL3 小时前
【机器学习】(20)—— 类别不平衡
人工智能·算法·机器学习
明志数科3 小时前
具身智能数据标准化:从碎片化接口到统一工作组的技术路径
人工智能·算法·机器学习