算法类型:双指针类型

双指针

1、移动零

移动零

示例如下:

1.1、题目理解

很明显,题目要求我们将数组分成两部分:

  • 前面一部分为非0数,并且顺序不可改变
  • 后面一部分全为0

1.2、原理分析

像这样类似数组分块 的问题,我们可以采用双指针的方法。

我们定义:

  • int dest = -1
  • int cur = 0

dest,全称destination,有目的地、终点的意思。我们这里让dest指向下标-1,意在使dest一直指向已排好的非0数序列的末端

cur,全称current,意为:当前的、现在的。cur的作用是遍历数组

cur遍历过程中:

  • 遇到下标对应值为0,什么也不做
  • 遇到下标对应值非0,dest++,dest与cur对应值交换

由于我们用的是for循环,cur的前进就不需要另外写。

我们以数组[0, 1, 0, 3, 12]为例,演示一遍移动的过程:

1.3、代码演示

cpp 复制代码
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        for (int dest = -1, cur = 0; cur < nums.size(); ++cur)
            if (nums[cur] != 0)
                swap(nums[cur], nums[++dest]);
    }
};

2、复写零

复写零

示例如下:

2.1、题目理解

遇到0就添加一个0,所以数组[1, 0, 2, 3, 0, 4, 5, 0]应该变成:

1, 0, 0, 2, 3, 0, 0, 4, 5, 0, 0

去除越界的部分,就得到了:

1, 0, 0, 2, 3, 0, 0, 4

2.2、原理分析

我们依旧使用双指针。

以数组[1, 0, 2, 3, 0, 4, 5, 0]为例。

从前往后,行不行呢?

图示过程中,我们使dest = 0,cur = 0,然后cur遍历整个数组,遇到非0数,dest++;遇到0,dest位置置0,dest--,dest位置再置0,dest--。

接着我们就会发现,cur指向第二个"0",对dest操作完后,元素"2"被覆盖掉了。所以从前往后不可行。

那从后往前呢?

我们使dest指向数组末尾,cur指向复写后数组的最后一个元素。cur开始向前遍历,遇到非0,cur指向数赋值给dest指向位置;遇到0,destdest位置置0,dest--,dest位置再置0,dest--。

就可以了。

现在的问题是:我们如何找到正确的cur,也就是复写0后数组的最后一个元素,在原来元素的位置?

我们还是可以使用双指针。

我们定义dest = -1,cur = 0,此时cur遍历数组,dest指向的是:复写操作调整完 之后的部分数组的末位

当cur指向非0,dest前进一位;当cur指向0,dest前进两位。

当dest指向原数组末位,结束。

但是还有一种情况:dest会越界。

比如数组[1, 0, 2, 3, 0, 4]:

越界时,dest一定等于arr.size(),此时cur一定指向了0。

这时,我们就要特殊处理:当dest == arr.size()时,数组末位置0,cur--,dest -= 2。

2.3、代码演示

cpp 复制代码
class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        int dest = -1, cur = 0, n = arr.size();
        // 1.找复写后数组的最后一个数
        while (cur < n)
        {
            if (arr[cur] != 0) ++dest;
            else dest += 2;
            if (dest >= n - 1) break;
            ++cur;
        }
        // 2.特殊处理
        if (dest == n)
        {
            arr[n - 1] = 0;
            --cur;
            dest -= 2;
        }
        // 3.复写数组
        while (cur >= 0)
        {
            if (arr[cur] != 0) arr[dest--] = arr[cur];
            else
            {
                arr[dest--] = 0;
                arr[dest--] = 0;
            }
            --cur;
        }
    }
};

3、快乐数

快乐数


3.1、题目理解

以19为例,画出19是快乐数的推导过程:

以2为例,画出2不是快乐数的推导过程:

3.2、原理分析

假设对的数的操作(每一次将该数替换为它每个位置上的数字的平方和)记为 f (操作)。

乍一看,快乐数与非快乐数之间没有任何联系。但是我们从循环的角度看,就可以把快乐数的推导过程与非快乐数的推导过程统一起来了:

  • 对于数a,当进行次若干f操作后,数a最终变为1,此时a无论经过多少次f操作,结果都是1。此时a为快乐数。
  • 对于数b,当进行次若干f操作后,数a最终变为非1的数c,此时b无论经过多少次f操作,结果都是c。此时b不是快乐数。

这时,我们就可以借鉴快慢指针的做法。

3.3、代码演示

cpp 复制代码
class Solution {
public:
    //平方和操作另外封装
    int Func(int n)
    {
        int happy = 0;
        while (n)
        {
            happy += (n%10)*(n%10);
            n /= 10;
        }
        return happy;
    }
    bool isHappy(int n) {
        //如果slow和fast都为n,那么循环就进不去
        int slow = n, fast = Func(Func(n));
        while (slow != fast)
        {
            slow = Func(slow);
            fast = Func(Func(fast));
        }
        return fast == 1;
    }
};

4、盛最多水的容器

盛最多水的容器

4.1、题目理解

我们以例一为例。

柱状图是以数组每一位数字的下标为横坐标,每一位上的数字值为纵坐标:

每两根柱子构成一个容器。由木桶效应,得较短的柱子决定了盛水的多少,而柱子的高度就是数组对应位置上的值。

设数组名为height,选取的数组两个位置上的值,左边的值下标为left,右边的下标为right。那么底L为:(right - left),高h就是较小数。

则最多盛水的计算公式:
最多盛水 = ( r i g h t − l e f t ) ∗ m i n ( h e i g h t [ l e f t ] , h e i g h t [ r i g h t ] ) 最多盛水=(right - left)*min(height[left], height[right]) 最多盛水=(right−left)∗min(height[left],height[right])

如上图,选取数组1位置下的值8,和8位置下的值7,则最多盛水:(8 - 1)*7 = 49。

4.2、原理分析

我们可以直接采取暴力解法,将数组能生成的所有容器都列举一遍,然后算出所有的容量,再选出最大的一个。但是此时的时间复杂度为O(N^2),提交时可能会超时。

我们可以这样思考:

我们取出部分数组:[6, 2, 5, 4],令left指向最左边的6,right指向最右边的4。此时底L为3,高h为4。

假设我们令left向前,也就是抛弃6,同时right不动。此时left指向2。那么这时left的移动过程中,L和H的变化情况为:

  • 底L一直减小
  • 高h在left的移动过程中,可能遇到大于4的值,此时h不变;也可能遇到小于4的值,此时h减小

由公式容量V = L * h,left舍弃了较大值6,接下来的left无论怎么与right组合,结果都减小。

4.3、代码演示

由此我们就可以设计代码:

定义双指针left指向数组首位、right指向数组末位。计算出当前容量。然后选出较小数,对应指针往里靠近,再计算容量。重复操作。

cpp 复制代码
class Solution {
public:
    int maxArea(vector<int>& height) {
        int left = 0, right = height.size() - 1, V = 0;
        while (left < right)
        {
            V = max(V, (right-left)*min(height[left], height[right]));
            if (height[left] < height[right]) ++left;
            else --right;
        }
        return V;
    }
};

5、有效三角形的个数

有效三角形的个数

5.1、题目理解

比如数组[2①, 2②, 3, 4],有以下组合:

其中不满足的,只有2, 2, 4,所以有效的三元组是3个。

比如数组[4, 2, 3, 4],有以下组合:

其中所有组合都是有效的,所以有效的三元组是4个。

5.2、原理分析

首先,我们对判断三个数是否构成三角形三条边的依据作一个优化:

我们之前判断三个数a, b, c是否构成三角形三条边,依据是:
a + b > c a+b>c a+b>c
a + c > b a+c>b a+c>b
b + c > a b+c>a b+c>a

三个条件同时满足。

但其实,我们对a, b, c三个数进行排升序,得到新序列x, y, z(x <= y <= z)。

只要满足两个较小数x+y > z,那么a, b, c(x, y, z)三个数一定能构成三角形。

所以,我们先对给出序列,排成升序

接着,使用双指针法。我们以序列[2, 2, 3, 4, 5, 9, 10]为例演示过程:

定义三个下标:

  • i从数组末位开始(直到下标2位置结束,因为是三元数),遍历数组
  • left起始指向0位置,right指向i的前一个位置

此时2 + 9 = 11 > 10。由于数组是递增的,此时left从0位置开始,一直到right的前一位数中,每一个数与right指向数相加,结果都大于10,都满足构成三角形的条件。

那么这时,我们统计到有效三角形个数为:right - left = 5 - 0 = 5。

然后,--right。

此时2 + 5 = 7 < 10,不构成有效三角形。我们就让left++,left不能与right相遇。此此时有两种情况:

  • 如果arr[left] + arr[right]还是小于arr[i],只能再left++,看看有没有加和大于arr[i]的情况
  • 如果arr[left] + arr[right]大于arr[i],就先统计有效三角形个数,再--right

此时,我们可以总结方案:

  • 外层循环中,i遍历数组
  • 内层循环中,left起始位置0,right起始位置i - 1,left不能与right相遇:
    • 如果arr[left] + arr[right] > arr[i] ,统计个数:right - left,然后--right
    • 如果arr[left] + arr[right] < arr[i] ,left++

5.3、代码演示

cpp 复制代码
class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        // 1.排序
        sort(nums.begin(), nums.end());
        // 2.统计
        int ret = 0, n = nums.size();
        for (int i = n - 1; i >= 2; --i)
        {
            int left = 0, right = i - 1;
            while (left < right)
            {
                if (nums[left] + nums[right] <= nums[i]) ++left;
                else ret += right - left, --right;//left后逗号不可改为分号,否则分支终止
            }
        }
        return ret;
    }
};

6、两数之和

两数之和

cpp 复制代码
class Solution {
public:
    vector<int> twoSum(vector<int>& price, int target) {
        int n = price.size(), left = 0, right = n - 1;
        while (left < right)
        {
            int sum = price[left] + price[right];
            if (sum < target) ++left;
            else if (sum > target) --right;
            else
            {
                return { price[left], price[right] };
            }
        }
        return {-1, -1};
    }
};

7、三数之和

三数之和


7.1、题目理解

怎么理解不重复

以数组[-1, 0, 1, 2, -1, -4]为例。

前一个-1组成的序列[-1, 0, 1]符合三数和为零,后一个-1组成的序列[-1, 0, 1]也符合三数和为零。

所以,我们有一个去重的需求。

7.2、原理分析

我们可以先排序

那么这时,我们可以直接枚举出所有的三元组,然后使用库函数set去重。

但是,我们可以做更进一步的优化:

  • 排完序之后,我们用下标i固定一个数a。

  • 然后在剩下的序列[i+1, size-1]中,定义下标left, right。然后寻找left与right指向数的和是否等于a的相反数。此时问题就变为了两数之和的问题。

但是,有一些细节需要处理:

  • 当i移动到a大于0之后,由于此时数组升序,之后就找不到两个数,和为a的相反数。此时的i就无需讨论。
  • 不漏
    • 之前两数之和只需要我们返回一种情况即可。而本题需要我们找到所有情况,所以找到一种情况后,left, right应继续向里靠近,继续寻找。
  • 去重
    • 找到一种情况后,我们假设此时 left 指向 x, right 指向 y 。当left, right向里靠近时,由于此时数组升序,可能指向相同的 x, y 。这时我们应该再让left, right向里靠近。
    • 当前指向a的i如果找到了符合的情况,向前遍历时可能也会找到相同的 a ,这时i也要跳过。
  • 防止越界
    • 如果序列为[0, 0, 0],去重过程中left可能越界,应注意控制

去重处理的辅助思考:

7.3、代码演示

参考代码:

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        
        vector<vector<int>> ret;
        //排序
        sort(nums.begin(), nums.end());

        int n = nums.size();
        for (int i = 0; i < n; )//固定a
        {
            //升序,a>0,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
                {
                    ret.push_back({nums[left], nums[right], nums[i]});
                    ++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] == nums[i-1]) ++i;
        }
        return ret;
    }
};

//我自己思路的小问题:找到情况后,left, right是同时向内一步的

我想出的代码:

cpp 复制代码
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        // 1.排序
        // 2.固定一个数a
        // 3.在该数后面的区间内,利用 "双指针算法" 快速找到两个和为 -a 的数

        // 细节
        // 1.不漏
        //   找到一种结果之后,不要停,left与right继续靠近,继续寻找
        // 2.去重
        //   找到一种结果后,由于数组有序,相同的值紧靠在边上,left与right要跳过当前结果对应的相同的值
        //   找到一种结果后,i也要跳过相同值
        // 3.防止越界
        //left,right容易越界,要确定范围
        sort(nums.begin(), nums.end());// 1.排序

        vector<vector<int>> vv;
        int n = nums.size();
        for (int i = 0; i < n-2; ++i)// 2.固定一个数a
        {
            if (nums[i] > 0) break;//a>0时的优化
            if (i != 0 && nums[i] == nums[i-1]) continue;//对固定数a的去重

            int left = i + 1, right = n-1;
            while (left < right)
            {
                if(left != i + 1 && right != n-1 && nums[left] == nums[left-1] && nums[right] == nums[right+1])
                {//对left和right指向数的去重
                    ++left; --right;
                    continue;
                }
                int sum = nums[left] + nums[right], target = -nums[i];
                if(sum > target) --right;
                else if(sum < target) ++left;
                else
                {
                    vv.push_back({nums[left],nums[right],nums[i]});
                    ++left; --right;//继续寻找
                }
            }
 
        }



        return vv;
    }
};

8、四数之和

四数之和

8.1、题目理解

与"三数之和"类似。

我们选出的每一个四元组,其中每一个数必须来自不同的位置;并且这四个数的不同组合,都算一个四元组。

8.2、原理分析

与"三数之和"类似。

  • 首先排成升序数组
  • 其次,从左往右,固定数a
  • 接着,在数a之后的序列中,固定数b
    • 在数b之后的序列中,起始时定义一首一尾下标left, right
    • 找到四数之和小于target,left++
    • 找到四数之和大于target,right--
    • 等于
      • 入序列
      • ++left, --right,这是为了不漏

去重

  • a要去重
  • b要去重
  • left, right要去重

8.3、代码演示

cpp 复制代码
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> ret;

        sort(nums.begin(), nums.end());// 排序
        int n = nums.size();
        //for (int i = 0; i < n - 3; )
        for (int i = 0; i < n; )//其实这里不需要特殊处理,因为超过了,操作left, right的循环就进不去了
        {// 固定a
            for (int j = i+1; j < n; )//这里也一样
            {// 固定b
                long long sum_i_j = nums[i] + nums[j];
                int left = j+1, right = n-1;
                while (left < right)
                {// long long防止数据溢出
                    long long sum_left_right = nums[left] + nums[right];
                    if (sum_i_j + sum_left_right < target) ++left;
                    else if (sum_i_j + sum_left_right > target) --right;
                    else
                    {//                     优化
                        ret.push_back({nums[left--], nums[right--], nums[i], nums[j]});
                        // 对left, right去重
                        while (left < right && nums[left] == nums[left-1]) ++left;
                        while (left < right && nums[right] == nums[right+1]) --right;
                    }
                }
                ++j;// 对b去重
                while (j < n - 2 && nums[j] == nums[j-1]) ++j;
            }

            ++i;// 对a去重
            while (i < n - 3 && nums[i] == nums[i-1]) ++i;
        }

        return ret;
    }
};
相关推荐
吴可可1231 小时前
三点绘圆弧的几何实现
算法
kyle~1 小时前
导航---LIO(激光雷达-惯性里程计)算法
c++·算法·机器人·ros2·导航
AGI前沿2 小时前
# 反内卷,回基础:Nano-Memory用极简检索与剪枝,解决大模型长对话遗忘
算法·机器学习
无限进步_2 小时前
【C++】私有虚函数与多态:访问权限不影响动态绑定
开发语言·c++·ide·windows·git·算法·visual studio
君鼎2 小时前
C++20 新特性全面总结
算法·c++20
枫叶机关录2 小时前
算法笔记:K-means、K-means++与K-Medoids聚类算法--详解、案例分析
算法·聚类·k-means
贾斯汀玛尔斯2 小时前
每天学一个算法-- 归并排序(Merge Sort)
数据结构·算法·排序算法
算法鑫探2 小时前
算法中的二分法(二分查找)详解及示例
c语言·数据结构·算法·新人首发
叶子野格2 小时前
《C语言学习:编程例题》8
c语言·开发语言·c++·学习·算法·visual studio