【ONE·基础算法 || 双指针】

在这里插入图片描述

总言

  主要内容:编程题举例,理解双指针思想。
  
  

  
  

1、双指针

  
  总言:常见的双指针有两种形式,一种是对撞指针,一种是左右指针。 (PS:在以数组形式为基础的结构中,这里的指针可以简化为数组元素下标。实际上,这里的双指针只是一个思想,在不同情景下可以用不同形式表示,并不一定要局限于指针变量本身。)
  

在这里插入图片描述对撞指针概述:

  对撞指针: 一般用于顺序结构中,也称左右指针。对撞指针从两端向中间移动。一个指针从最左端开始,另一个从最右端开始,然后逐渐往中间逼近。
  对撞指针的终止条件: 一般是两个指针相遇或者错开 (也可能在循环内部找到结果,直接跳出循环),也就是:

left == right (两个指针指向同⼀个位置)
left > right (两个指针错开)

  
  

在这里插入图片描述快慢指针概述:

  快慢指针: ⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。
  常用实现方式: 在一次循环中,每次让慢的指针向后移动一位,而快的指针往后移动两位,实现一快一慢。(此外还有其它实现方式,看情况而定。)
  场景举例: 这种方法对于处理环形链表数组非常有用。此外,若要研究的问题出现循环往复的情况时,均可考虑使用快慢指针的思想。

  
  
  

2、零移动(easy)

2.1、思路分析

  题源:链接

在这里插入图片描述

  
  1)、思路说明
  此类题属于数组划分,即根据⼀种划分方式(题目条件),将数组的内容分成左右两部分,不同区间具有不同属性。这种类型的题,⼀般就是使⽤「双指针」来解决。两指针划分区间,指针移动过程中,保持各区间属性不变。

  
在这里插入图片描述

  如上图:两个指针dest、cur。

  cur指针:从左到右遍历数组,在扫描的过程中,根据遇到的情况分类处理,实现数组的划分。此题中:

[0,cur-1]:该区间内元素属于已经处理完成的部分(满足左侧非零元素右侧零元素);
[cur,n):该区间内元素等待被处理。知道cur遍历到数组末尾,所有元素处理完毕。

  dest指针:数组按条件划分的分界点。在此题中,以零元素将区间分为两段,因此dest指针用来记录非零数序列的最后⼀个位置。

[0,dest]:该区间内元素均为非零元素。
[dest+1,cur-1]:该区间内元素均为零元素。

  
  
  2)、如何判断移动?

  初始时: cur = 0 ,用于遍历数组; dest = -1 指向非零元素序列的最后⼀个位置。(满足三区间属性:非零元素区、零元素区、待处理区)
  cur从左到右遍历: 对元素排查处理,会遇到两种情况。但无论遇到哪种情况,cur++(处理非零元素、零元素的任务是由desc指针来做的)。

cur遍历遇到0,没desc的事,cur++;
cur遍历遇到非0,要将该元素划分入desc指针左侧(此部操作由desc指针和cur指针置换完成),cur++;

  desc处理分界线操作: 始终要记住这里 [0, dest] 的元素全部都是非零元素, [dest + 1, cur - 1] 的元素全是零。

当cur遇到一个非零元素,意味着desc左侧范围要新增一个位置,由于这里是进行元素交换,因此需要先让desc+1

在这里插入图片描述

  
  
  

2.2、题解

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int dest,cur;
        dest = -1; cur = 0;

        while(cur < nums.size())
        {
            if(nums[cur])//cur遍历过程中,遇到符合情况的元素,才会改动dest分界线
            {
                swap(nums[dest++ + 1], nums[cur]);//先交换数据,再让dest++。可分开成两条语句来写。
            }
            cur++;//而cur本身作为遍历的指针,无论哪种情况(0/非0)都要向后挪动排查寻找,故可统一来写。
        }
    }
};

  再次精简版:

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        for (int cur = 0, dest = -1; cur < nums.size(); cur++)
            if (nums[cur]) // 处理⾮零元素
                swap(nums[++dest], nums[cur]);
    }
};

  
  
  
  
  
  
  

3、复写零(easy)

  题源:链接

在这里插入图片描述

  

3.1、题解

  1)、思路分析
  实际原地复写是在异地复写的基础上演变而来的。若是异地复写,则直接遍历数组,用一个新的vector按照要求进行尾插即可。
在这里插入图片描述

  而对于在原地复写,若从左向右进行原地复写操作的话,由于 0 的出现会复写两次,会导致没有复写的数被覆盖掉。
在这里插入图片描述

  
  因此可以选择从右往左的复写策略。但是从右向左复写时,需要找到最后⼀个复写的数。扩展: 此题中需要从右向左复写,但不能对此死板固定,在有些题中原地复写需要从左向右,比如:原地移除数组中值为val的元素。)
在这里插入图片描述

  
  故上述整体流程如下:
  Ⅰ、先找到最后一个复写的数。 (这里只找数,不做修改。找数使用的也是双指针法,从左往右找数):

1.先判断cur位置的值,决定dest向后移动一步或者两步。
2.移动后,判断一下dest是否已经到结束位置。
4.cur++,重复操作,直到循环结束。

在这里插入图片描述

  

  Ⅱ、在上述步骤中,需要处理特殊的边界情况。 在从左向右遍历找最后一位复写数的过程中,如果 dest 超过了数组的最大索引 n-1,则说明没有足够的空间来放置所有的0。在这种情况下,将数组的最后一个元素设置为0,并将 dest 和 cur 指针向前移动相应间隔。
在这里插入图片描述

  
  Ⅲ、 在完成上述准备工作后,即可开始使用双指针从后向前进行复写。
  

class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        // 1. 找到最后一次复写数
        int cur = 0, dest = -1, n = arr.size();
        while (cur < n) {
            if (arr[cur])//cur指向非零数
                dest++;
            else//cur指向零,dest复写(这里只找位置)
                dest += 2;
            if (dest >= n - 1)//判断 dest 是否已经到结束位置
                break;
            cur++;
        }
        // 2. 处理⼀下边界情况
        if (dest == n) {
            arr[n - 1] = 0;
            cur--;
            dest -= 2;
        }
        // 3. 从后向前完成复写操作
        while (cur >= 0) {
            if (arr[cur]) //cur不为零时
                arr[dest--] = arr[cur--];
            else {  //cur为零时,需要复写
                arr[dest--] = 0;
                arr[dest--] = 0;
                cur--;
            }
        }
    }
};

  
  
  
  
  
  
  
  
  
  

4、快乐数(medium)

4.1、思路分析

  题源:链接

在这里插入图片描述
  
  1)、思路说明
  根据题目可知,对于该正整数,无论最后结果是否为快乐数,⼀定会进入死循环。只不过快乐数的环为1 → 1 → 1 → 1…(元素始终为1);不快乐数的环中含有各种数字。

在这里插入图片描述
  既然该变化的过程最终会形成一个圈,对于此类问题可以用快慢指针来解决。快慢指针的特性: 在⼀个圆圈中,快指针总是会追上慢指针的,也就是说他们总会相遇在⼀个位置上。 因此,若相遇位置的值是 1 ,那么这个数⼀定是快乐数;如果相遇位置不是 1 的话,那么就是不快乐数。
  
  
  2)、扩展:证明无论如何都会相遇(鸽巢原理)
在这里插入图片描述

  题目说明: 1 < = n < = 2 31 − 1 1 <= n <= 2^{31} - 1 1<=n<=2311 ,即 2,147,483,648 共10位数,要求对 n n n 的每位数上的数字进行平方和,我们选该位数下最大的数来验证。
  假设 n = 9999999999 n = 9999999999 n=9999999999,则 9 2 + 9 2 + 9 2 + … … = 9 2 × 10 = 810 9^2 +9^2+9^2+……=9^2×10 =810 92+92+92+……=92×10=810,即 n n n 范围内,新数所得计算结果 [ 1 , 810 ] [1, 810] [1,810] 之间。

  以最坏的情况来假设(变化了810次所获得结果均无重复),根据鸽巢原理,当变化超过810次后(即 ≥ 811 ≥ 811 811),所获得的结果仍旧在 [ 1 , 810 ] [1, 810] [1,810] 范围内,则必然会有一个数重复,如此构成循环,因此可以⽤快慢指针来解决。
  
  
  

4.2、题解

  如下:

#include <math.h>
class Solution {
public:
    int fun(int n) { //用于计算数的平方:注意这里数学函数使用,也可以直接计算。
        int ret = 0;
        while (n) {
            ret += pow(n % 10, 2);
            n /= 10;
        }
        return ret;
    }

    bool isHappy(int n) {
        int slow = n;
        int fast = fun(n);

        while (slow != fast) {
            slow = fun(slow);
            fast = fun(fun(fast));
        }

        return slow == 1 && fast == 1;
    }
};

  
  
  
  
  
  
  
  
  
  

5、盛水最多的容器(medium)

  题源:链接

在这里插入图片描述
  

  

5.1、暴力求解(穷举所有情况)

  说明: 最先能想到的是应该是穷举所有情况(暴力解法),列出能构成的所有容器情况,找出其中容积最大的值,该法下时间复杂的是 O ( n 2 ) O(n^2) O(n2) ,在OJ题中存在超时可能。

class Solution {
public:
    int maxArea(vector<int>& height) {
        int n = height.size();
        int ret = 0;
        // 两层 for 枚举出所有可能出现的情况
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                // 计算容积,找出最⼤的那⼀个
                ret = max(ret, min(height[i], height[j]) * (j - i));
            }
        }
        return ret;
    }
};

  在此基础上优化,看看有没有其它解法。
  
  
  
  

5.2、单调性+双指针

  1)、思路分析

  说明: 设两指针 i , j ,指向的水槽板高度分别为 h[i] , h[j] ,此状态下水槽面积为 S(i,j) 。由于可容纳水的高度由两板中的 短板 决定,因此可得面积公式 : S ( i , j ) = m i n ( h [ i ] , h [ j ] ) × ( j − i ) ,即 S = 高度 × 宽度。 S(i,j)=min(h[i],h[j])×(j−i),即S=高度×宽度。 S(i,j)=min(h[i],h[j])×(ji),即S=高度×宽度。
在这里插入图片描述

  对长度: 在每个状态下,无论长板或短板向中间收窄一格,都会导致水槽 底边宽度 −1变短。
  对高度:
  若向内移动长板 ,会出现两种情况,①新板比当前短板高,此时水槽高度不变;②新板比当前短板矮,此时水槽高度减小。最终计算的水槽高度 min(h[i],h[j])不变或变小,根据计算公式,水槽的面积一定变小 。
  若向内移动短板 ,新板有可能比当前短板高,此时水槽高度 min(h[i],h[j])可能变大,因此下个水槽的面积可能增大 。
在这里插入图片描述
  因此可以根据上述规律,先确定leftright左右两板,然后选出短板,向内移动。由此,就能减少解法一中穷举的部分情况。
  
  
  2)、题解
  PS:要注意体积的计算和这里下标的关系。该写法时间复杂的是 O ( n ) O(n) O(n) 。这里left、right下标充当了双指针,故本质上此题是双指针的解法。

class Solution {
public:
    int maxArea(vector<int>& height) {
        int max = 0; // 用于记录每次获取到的容器最大体积
        int left = 0; //左侧板下标
        int right = height.size()-1; //右侧板下标

        while(left < right) //结束条件:两板相遇(两指针相遇)
        {
            int volume = min(height[left],height[right])*(right-left); //计算当前指向的容积大小
            max = max > volume ? max: volume;//判断是否容积最大
            //其它写法:max = max(max,volume);

            if(left < right && height[left] < height[right]) //向内移动短板
                left++;
            else
                right--;
        }
        return max;
    }
};

  再简化版:其实都一样,只是这种写法下,会少判断几次。

class Solution {
public:
    int maxArea(vector<int>& height) {
        int left = 0, right = height.size()-1;
        int ret = 0;
        while(left < right)
        {
            ret = max(ret, (right - left)* min(height[left],height[right]));
            // 判断短板,移动短板
            if(height[left] < height[right])// 左侧板更短,左侧板向右移动
            {
                ++left;
                while(left < right && height[left] < height[left-1]) left++;
            }
            else
            {
                --right;
                while(left < right && height[right] < height[right+1]) right--;
            }
        }
        return ret;
    }
};

  
  
  
  
  
  

  
  
  
  

6、有效三角形的个数(medium)

  题源:链接

在这里插入图片描述
  判断三条边能组成三角形的条件为: 任意两边之和大于第三边,任意两边之差小于第三边。
  
  

6.1、暴力求解

  说明: 使用三层循环枚举所有情况。时间复杂度为 O ( n 3 ) O(n^3) O(n3),可能会超时。

class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        // 1. 排序
        sort(nums.begin(), nums.end());
        int n = nums.size(), ret = 0;
        // 2. 从⼩到⼤枚举所有的三元组
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                for (int k = j + 1; k < n; k++) {
                    // 当最⼩的两个边之和⼤于第三边的时候,统计答案
                    if (nums[i] + nums[j] > nums[k])
                        ret++;
                }
            }
        }
        return ret;
    }
};

  
  
  
  
  

6.2、单调性+双指针

  1)、思路说明

  ①排序,优化判断三角形成立的条件: 根据题目,对于任意a、b、c,要满足三角形意味着任意两边之和大于第三边,则代表要判断三次(a+b>c、a+c>b、b+c>a),所以可以先给数组排个序再来判断。

  ②双指针扫描,确定三边: 固定⼀个最长边,然后在比这条边小的有序数组中找出⼀个⼆元组,使这个⼆元组之和大于这个最长边。
在这里插入图片描述
  此时可能出现的情况:
   a、nums[left] + nums[right] < nums[i] 。①若此时right向左移动,则nums[left] + nums[right]的值只会更小。②只有left向右移动,nums[left] + nums[right]的值才有可能变大。③综上,该情况下,left 位置的元素可以舍去, left++ 进入下轮循环。

  b、nums[left] + nums[right] > nums[i] 。 ①若此时left向右移动,则nums[left] + nums[right] > nums[i]仍旧成立,意味着 [left, right - 1] 区间上的所有元素均可以与 nums[right] 构成比nums[i] 大的二元组,可直接计算存在的总数:b - a 。②此时 right 位置的元素的所有情况相当于全部考虑完毕,让 right-- ,进入下一轮判断。
  
  
  
  2)、题解
  相较于暴力解法,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  其它:要注意理解这里sum统计出的对数结果为什么是right - left,而非right - left + 1统计。

class Solution {
public:
    int triangleNumber(vector<int>& nums) {

        sort(nums.begin(), nums.end()); // 排序

        int sum = 0; // 用于记录总数
        for (int i = nums.size() - 1; i >= 2; --i) {
            // 在nums[i]左区间内用双指针遍历寻找符值
            int left = 0;
            int right = i - 1;
            while (left < right) {
                if (nums[left] + nums[right] > nums[i]) // 满足条件
                {
                    sum += right - left; // 统计出当前区间段内满足条件的总次数
                    right--; // 进⼊下轮循环
                } else {
                    left++; // 不满足条件,让left++增大,进⼊下轮循环
                }
            }
        }

        return sum;
    }
};

  
  
  
  
  
  
  
  
  
  
  
  

7、查找总价格为目标值的两个商品(easy)

  题源:链接

在这里插入图片描述

  
  此题解法有多种,这里仅作部分举例。
  
  
  

7.1、暴力求解

  说明: 双层循环,定义num1、num2,判断 num1 + num2 = target 即可。该解法时间复杂度为 O ( n 2 ) O(n^2) O(n2),但相比之下没有利用到给定数组有序这一条件。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int n = nums.size();
        for (int i = 0; i < n; i++) { // 第⼀层循环从前往后列举第⼀个数
            for (int j = i + 1; j < n; j++) 
            { // 第⼆层循环从 i 位置之后列举第⼆个数
                if (nums[i] + nums[j] == target) // 两个数的和等于⽬标值,说明我们已经找到结果了
                    return {nums[i], nums[j]};
            }
        }
        return {-1, -1};
    }
};

  
  
  
  

7.2、二分查找

  说明: num1 + num2 = targetnum1 = target - num2。如此,只需要固定其中一位数,再在有序数组中二分查找另一位数即可。这种做法下,优化了查找速度,时间复杂度为 O ( n ∗ l o g 2 n ) O(n*log_2n) O(nlog2n)

class Solution {
public:
    vector<int> twoSum(vector<int>& price, int target) {

        // 固定其中一个价格
        for (int i = 0; i < price.size(); ++i) {
            // 在给定值中,用二分查找一个价格
            int num = target - price[i];

            int left = 0;
            int right = price.size() - 1;
            while (left < right) {
                int mid = (left + right) / 2;// [0,mid][mid+1, size()-1]
                if (price[mid] >= num) // 在左边:[0,mid]
                    right = mid;
                else  // 在右边:[mid+1, size()-1]
                    left = mid + 1;
            }
            if(price[left] == num)
                return {price[i],price[left]};
        }
        return {};
    }
};

  
  
  
  
  

7.3、单调性+双指针

在这里插入图片描述
  该解法时间复杂度为 O ( n ) O(n) O(n)

class Solution {
public:
    vector<int> twoSum(vector<int>& price, int target) {
        // 定义双指针:这里是数组下标
        int num1 = 0;
        int num2 = price.size() - 1;
        while (num1 < num2) {
            int sum = price[num1] + price[num2];
            if (sum < target) // 说明需要将sum值增大,升序数组,只能让左指针右移
                num1++;
            else if (sum > target) // 说明需要将sum值减小,升序数组,只能让右指针左移
                num2--;
            else 
                return {price[num1] , price[num2]};
        }
        return {};//找不到的情况
    }
};

  
   扩展延伸: 上述三种方法一步步走来,我们解决的是如何找→如何快速找到 这一过程,实际上题目只要求找满足条件的一组值,扩展一下,若是要找满足条件的所有组合,又该如何做?

   由于数组有序,只需要继续向内部寻找即可。实际下述的例题就需要如此处理。
在这里插入图片描述
  
  
  
  
  
  
  
  
  
  

8、三数之和(medium)

  题源:链接

在这里插入图片描述

  

8.1、单调性+双指针

  1)、思路分析

  说明: 此题仍旧可以使用暴力解法,先排个序,再枚举出所有情况,由于题目要求三元组不重复,因此可以考虑使用set等容器去重处理,其时间复杂度为 O ( n 3 ) O(n^3) O(n3)
  
  
  这里我们使用双指针思想进行优化。实际上本题为上一题的加强版,num1 + num2 + num3 = 0num1 + num2 = - num3,如此基本思想和上题类似:
  Ⅰ. 先排序;
  Ⅱ. 然后固定⼀个数 num3;
  Ⅲ. 在该数后的区间内,使用「双指针算法」快速找到两个数之和等于 - num3 即可。

  但需要注意两点:
  1)、如何找全所有组合? 在找到一组后,继续缩小区间范围寻找。
  2)、如何去重? 与上一题不同的是,这里需要考虑组合数重复的情况,即[-1,0,1][0,1,-1]是同一组合。因此,如果不用容器去重,手动去重可借助排序,一次性排除每回合相同的固定值。
在这里插入图片描述

  
  
  2)、题解

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        // 1、排序
        sort(nums.begin(), nums.end());

        vector<vector<int>> ret;

        // 2、找三元组
        for (int i = 0; i < nums.size(); ) {

			if(nums[i] > 0) break;//一个小优化:当固定数为正数时,由于已经排升序,此时固定数右侧区间内找不到任意两数和为负数
			
            // 准备工作
            int left = i + 1; // 左指针
            int right = nums.size() - 1;// 右指针
            int target = -nums[i]; // 两数相加等于相反数

            // 找数
            while (left < right) {
                int sum = nums[left] + nums[right];
                if (sum < target) {left++;} 
                else if (sum > target) {right--; } 
                else {
                    // 当前固定数下找到一组,存放当前组合,继续找。
                    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;
            while (i < nums.size() && nums[i] == nums[i - 1]) ++i;
        }

        return ret;
    }
};

  
  
  
  
  
  
  
  
  

9、四数之和(medium)

  题源:链接

在这里插入图片描述

  
  

9.1、单调性+双指针

  此题核心思想与上一题相同,只是在原先基础上多加了一层。nums[a] + nums[b] + nums[c] + nums[d] = targetnums[b] + nums[c] + nums[d] = target - nums[d],转换一下,四数就变成了原来的三数。
  
  Ⅰ、 依次固定⼀个数 a ;
  Ⅱ、 在这个数 a 的后面区间上,利用「三数之和」找到三个数,使这三个数的和等于 target - a 即可。
  这里仍旧需要去重+找全。
在这里插入图片描述

  

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {

        vector<vector<int>> ret;
        //1、排序
        sort(nums.begin(),nums.end());
        //2、找组合数
        int n = nums.size();

        for(int a = 0; a < n ; )
        {
            for(int b = a + 1; b < n; )
            {
                int c = b+1;
                int d = n-1;
                long long aim = (long long)target -nums[a] -nums[b];

                while(c < d)//双指针找数
                {
                    int sum = nums[c] +nums[d];
                    if(sum > aim) { d--;}
                    else if(sum < aim) { c++; }
                    else
                    {
                        //找到符合条件的一组
                        ret.push_back({nums[a],nums[b],nums[c],nums[d]});
                        c++; d--;
                        //去重
                        while(c < d && nums[c] == nums[c-1]) { c++; }
                        while(c < d && nums[d] == nums[d+1]) { d--; }
                    }
                }
                //当前b指向的值找完,去重后继续下一轮
                b++;
                while(b < n && nums[b] == nums[b-1]) b++;
            }
            //当前a指向的值找完,去重后继续下一轮
            a++;
            while(a < n && nums[a] == nums[a-1]) a++;
        }
        return ret;
    }
};


  
  
  
  
  
  
  
  
  
  

Fin、共勉。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值