双指针

1.移动零

在这里插入图片描述

属于一类数组划分或数组分块题型,使用双指针算法,类似快排的双指针法,以a[key]将数组划分为<=a[key]部分和>a[key]部分。

定义两个指针cur、dest。

cur:从左往右扫描数组,即遍历数组。

dest:在已处理区间内,记录非0元素最后一个位置。

首先cur会把数组划分为已处理区间和未处理区间 [0,cur-1]和[cur,n-1]。

在已处理区间,dest会分出[0,dest]和[dest+1,cur-1],[0,dest]为相对顺序不变的非0数,[dest+1,cur-1]全为0。

关键:时刻保证

[0,dest] ——》非0

[dest+1,cur-1] ——》0

[cur,n-1] ——》待处理

如何做到:

dest先置为-1,cur为0,则一开始无[0,dest],[dest+1,cur-1]。随着cur++,[0,dest],[dest+1,cur-1]两个区间开始变化,可以假定现在[0,dest]全为非0,[dest+1,cur-1]全为0。当cur遇到非0时,要把a[cur]放到[0,dest]里,所以区间[0,dest]要扩大,即dest++,然后交换a[dest]和a[cur]。dest++后,a[dest]落在全为0的区间,此时交换就把0交换出非0区间了。

在这里插入图片描述

代码实现

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int dest = -1;
        int cur = 0;
        for (int i = 0;i < nums.size();i++)
        {
            if (nums[cur] != 0)
            {
                dest++; //扩大非0区间
                swap(nums[cur], nums[dest]);
            }
            cur++;  //cur扫描数组
        }
    }
};

2.复写零

在这里插入图片描述

使用 双指针算法 ,先根据异地操作,再优化成双指针的就地操作。

异地操作即另开一个数组,定义cur指向首元素,dest指向新数组的第一个位置。cur遍历原数组,arr[cur]不为0时,dest对应为arr[cur],dest++;arr[cur]为0时,dest和dest+1对应为0,dest+=2。最后就完成了异地复写。

在这里插入图片描述

就地操作分2步:

  1. 用双指针算法找到最后一个复写的数。
  2. 为避免覆盖,选择“从后向前”完成复写操作。

对于第1步,

  1. 一开始cur置为0,dest置为-1,cur遍历数组,判断arr[cur]的值。

  2. arr[cur]不为0,dest向后走一步,arr[cur]为0,dest向后走两步。

  3. dest每次走完后,判断dest是否已经到arr.size()-1,若是,则循环结束。

  4. 每次循环都要cur++。

在这里插入图片描述

第1步的特例,arr[]={1,0,2,3,0,4}。因为第1步出循环的条件是cur>arr.size()-1或dest>=arr.size()-1,对于这个特例,最后一次循环中,arr[cur]=0,dest+=2后越界,跳出循环。

在这里插入图片描述

此时从后往前复写即把arr[dest]和arr[dest-1]置为0,这样会越界访问,所以直接把arr[dest-1]置为0,然后dest-=2,cur–。

class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        int cur = 0;
        int dest = -1;
        int n = arr.size();
        while (cur < n)//第1步
        {
            if (arr[cur])
                dest++;
            else
                dest += 2;
            if (dest >= n - 1)//
                break;
            cur++;
        }
        if (dest == n)//越界处理特例
        {
            arr[dest - 1] = 0;
            dest -= 2;
            cur--;
        }
        while (cur >= 0)//第2步复写
        {
            if (arr[cur])
                arr[dest--] = arr[cur--];
            else
            {
                arr[dest--] = 0;
                arr[dest--] = 0;
                cur--;
            }
        }
    }
};

3.快乐数

在这里插入图片描述

以19和2为例:

在这里插入图片描述

可以看到,一个数经过快乐数的操作,最终都会进入一个循环,只要最终在1循环,就是快乐数。类似判断链表是否带环,所以此题可用快慢指针求解,当两指针相遇时,说明有循环,如果在1循环,即slow=1,则是快乐数。

由鸽巢原理,最终是一定会进入循环的。

在这里插入图片描述

810个数,经过大于810次操作后,必然会出现重复的数,即循环。

class Solution {
public:
    int Sum(int n)//快乐数操作
    {
        int sum = 0;
        while (n)
        {
            int ret = n % 10;
            sum += ret * ret;
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n)
    {
        int slow = n;
        int fast = Sum(n);//先拉开距离,两指针才能进入while循环
        while (slow != fast)
        {
            slow = Sum(slow);//注意更新n
            fast = Sum(Sum(fast));
        }
        if (slow == 1)
            return true;
        return false;
    }
};

4.盛最多水的容器

在这里插入图片描述
在这里插入图片描述

解法1:暴力枚举,两两取垂线,得出一系列体积,再从这些体积中找到最大值。

在这里插入图片描述

解法2:找规律,发现单调性,使用双指针解决问题。

在这里插入图片描述

任取一段区间[i,j],定住min(height[i],height[j])对应的height[i]或height[j],上图即定住4,发现如果区间缩小,体积Vij一定比一开始区间求出体积小,所以,值小的一端可以不用考虑。所以定义left=0,right=height.size()-1,谁小谁往中间移动,二者相遇时结束,这个过程中会得到一系列体积,用max记录最大体积。

class Solution {
public:

    int maxArea(vector<int>& height)
    {
        int n = height.size();
        int left = 0;
        int right = n - 1;
        int max = 0;
        int v = 0;
        while (left != right)
        {
            if (height[left] < height[right])
            {
                v = height[left] * (right - left);
                left++;
            }
            else
            {
                v = height[right] * (right - left);
                right--;
            }
            if (max < v)
                max = v;
        }
        return max;
    }
};

5.有效三角形的个数

在这里插入图片描述

给3个数,看能否构成三角形,需同时满足3个条件:a+b>c,a+c>b,b+c>a。

解法1:暴力枚举

对于所给数组,先用循环固定一个数,再用循环固定第二个数,最后再来一层循环遍历剩下的数并判断是否满足三角形的条件。

//伪代码
for(int i=0;i<n;i++)
	for(int j=i+1;j<n;j++)
		for(int k=j+1;k<n;k++)
		{
			check(nums[i],nums[j],nums[k]);//判断是否构成三角形
		}

时间复杂度为O(N^3),显然是不行的。

解法2:找规律,利用单调性,使用双指针算法。

如果三角形三边a,b,c是有序的,即a<=b<=c,由于c是最大的,那么只要判断a+b>c即可。所以,对于给定数组nums,先进行升序排序。

例如,排序后的nums为[2,2,3,4,5,9,10]。

先固定最大的数nums[n-1]作为c,定义left=0,right=n-2,显然nums[left]<=nums[right]。若nums[left]+nums[right]>nums[n-1],定住nums[right],剩下的元素都比nums[left]大,所以有right-left个三元组能构成三角形。

在这里插入图片描述

接下来right–,继续上述操作,若nums[left]+nums[right]<=nums[n-1],则left++,由于是升序数组,nums[left]才会变大。当left和right相遇时,缩小区间,固定最大数nums[n-2]。如此循环,直到最大数为nums[0]。

在这里插入图片描述

class Solution {
public:
    int triangleNumber(vector<int>& nums)
    {
        sort(nums.begin(), nums.end());//排序优化
        int sum = 0;  //构成三角形的三元组数
        for (int i = nums.size() - 1;i >= 0;i--)
        {
            int left = 0;
            int right = i - 1;
            while (left < right)
            {
                if (nums[left] + nums[right] > nums[i])
                {
                    sum += (right - left);
                    right--;
                }
                else
                {
                    left++;
                }
            }
        }
        return sum;
    }
};

6.和为s的两个数

在这里插入图片描述

解法1:暴力枚举

在这里插入图片描述

解法2:发现单调性,使用双指针

定义left=0,right=n-1,sum=price[left]+price[right]。因为数组price是升序的,如果sum<target,由于price[right]是数组中最大的,所以left++,使price[left]增大,sum才会增大;如果sum>target,则sum要减小,只能让right–,sum才减小;当sum==target时,即得所求结果。在循环中,如果left与right相遇,则说明数组中没有两个数相加等于target。

在这里插入图片描述

class Solution {
public:
    vector<int> twoSum(vector<int>& price, int target) 
    {
        vector<int> v;
        int n=price.size();
        int left=0;
        int 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 v;
    }
};

7.三数之和

在这里插入图片描述

解法1:排序,暴力枚举,set去重。

对于去重,把枚举结果放入vector<vector<int>> vv中,再用迭代器区间构造传给set<vector<int>> s去重,再用迭代器区间构造传回vv。这个时间复杂度为O(N^3),显然是不行的。

解法2:排序 + 双指针

先将数组排成升序,i=0,遍历数组,在i后面的区间用双指针快速找到两个数的和为 -nums[i],用前面“和为s的两个数”的类似方法求解,nums[i]、nums[left]、nums[right]即为所求三元组。

在这里插入图片描述

处理细节:

  1. 不漏

    找到一种结果后不要停,缩小区间,继续寻找,避免遗漏。

  2. 去重

    找到一种结果后left和right要跳过重复元素;当使用完一次双指针算法后,即left与right相遇,i也要跳过重复元素。同时都要避免越界。

小优化:

当nums[i]>0时,nums[left]+nums[right]>0是必然的,此时nums[left]+nums[right]一定不等于-nums[i],无需继续遍历。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums)
    {
        vector<vector<int>> vv;
        sort(nums.begin(), nums.end());//先排为升序
        int n = nums.size();
        for (int i = 0;i < n;)
        {
            if (nums[i] > 0)//小优化
                break;
            int target = (-nums[i]);
            int left = i + 1;
            int right = n - 1;

            while (left < right)//双指针
            {
                int sum = nums[left] + nums[right];
                if (sum < target)
                    left++;
                else if (sum > target)
                    right--;
                else
                {
                    vv.push_back({ nums[i],nums[left],nums[right] });
                    left++;
                    right--;
                    
                    //双指针去重
                    while (left < right && nums[left] == nums[left - 1])
                        left++;
                    while (left < right && nums[right] == nums[right + 1])
                        right--;
                }
            }
            i++;
            while (i < n && nums[i] == nums[i - 1])//i去重
                i++;
        }
        return vv;
    }
};

8.四数之和

在这里插入图片描述

与三数之和类似,相当于里面套了一个三数之和。先将数组排成升序,然后固定一个数,在该数后面的区间进行三数之和操作,即再定一个数,在其后面用双指针,使nums[left]+nums[right]==target-nums[i]-nums[j],四元组nums[i]、nums[j]、nums[left]、nums[right]即为所求结果。

class Solution11 {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target)
    {
        vector<vector<int>> vv;
        sort(nums.begin(), nums.end());//先排成升序
        int n = nums.size();
        if (n < 4) //元素数量不足,无法构成四元组
            return vv;

        for (int i = 0;i < n;)
        {
            if (nums[i] > 0 && target < 0)
                break;
            for (int j = i + 1;j < n;)
            {
                int left = j + 1;
                int right = n - 1;
                long long t = (long long)target - nums[i] - nums[j];
                while (left < right)
                {
                    int sum = nums[left] + nums[right];
                    if (sum < t)
                    {
                        left++;
                    }
                    else if (sum > t)
                    {
                        right--;
                    }
                    else
                    {
                        vv.push_back({ nums[i],nums[j],nums[left],nums[right] });
                        left++;
                        right--;
                        //去重一
                        while (left < right && nums[left] == nums[left - 1])
                            left++;
                        while (left < right && nums[right] == nums[right + 1])
                            right--;
                    }
                }
                j++;
                while (j < n && nums[j] == nums[j - 1])//去重二
                    j++;
            }
            i++;
            while (i < n && nums[i] == nums[i - 1])//去重三
                i++;
        }
        return vv;
    }
};

更多推荐