专题一_双指针_算法专题详细总结

目录

双指针

对撞指针:⼀般⽤于顺序结构中,也称左右指针。

快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列 结构上移动。

1. 移动零(easy)

「数组分两块」是⾮常常⻅的⼀种题型,主要就是根据⼀种划分⽅式,将数组的内容分成左右两部 分。这种类型的题,⼀般就是使⽤「双指针」来解决。

解题思路:

1.暴力求解:

2.双指针

算法总结:

2. 复写零(easy)

1.暴力求解:

2.双指针

1).寻找最后一个进行复写的元素

2).处理边界情况

3).从后向前进行复写

优化:

3. 快乐数(medium)

1.简单证明:

2.但是我后来发现,其实只用hash表+字符串来做这题也十分简单

4. 盛⽔最多的容器(medium)

1.解法⼀(暴⼒求解)(会超时):

2.双指针,很难想

5. 有效三⻆形的个数(medium)

1.解法⼀(暴⼒求解)(会超时):

2.很难,很有意义的双指针

6、LCR 179.查找总价格为目标值的两个商品

1.暴力:(超时)

2.双指针(缩小范围):

7. 三数之和(medium)

1.暴力:想都不用想 O(N^3) 绝对超时;

2.排序+双指针

8.四数之和(medium)

双指针:


算法专题——双指针,根据自己的学习总结出的多种双指针应用场景和使用方法。

一般来说,在面对数组算法题,极大可能考虑排序数组sort() 会优化时间复杂度,提高解题效率,提供解题思想,那么在数组排序后就会优先选择 双指针 和 二分查找两种算法,今天先来介绍双指针,一般情况下,在多维时间复杂度的情况下,利用双指针可以直接降低一维时间复杂度,极大的优化了算法的性能。

双指针

常⻅的双指针有两种形式,⼀种是对撞指针,⼀种是左右指针。

对撞指针:⼀般⽤于顺序结构中,也称左右指针。

• 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼 近。

• 对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循 环),也就是:

◦ left == right (两个指针指向同⼀个位置)

◦ l eft > right (两个指针错开)

快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列 结构上移动。

这种⽅法对于处理环形链表或数组⾮常有⽤。

其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快 慢指针的思想。

快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:

• 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。

1. 移动零(easy)

「数组分两块」是⾮常常⻅的⼀种题型,主要就是根据⼀种划分⽅式,将数组的内容分成左右两部 分。这种类型的题,⼀般就是使⽤「双指针」来解决。

解题思路:

1.暴力求解:

两层循环嵌套,第一层来寻找0的位置,第二层将所有元素往前移动,最后补0;

2.双指针

数组分块,首先就是双指针,
[0,dest] 非0 [dest+1,cur-1] 全部为0 [cur,n-1] 待处理
刚开始让dest=-1,cur=0,当nums[cur]==0 就一直cur++;当cur!=0 就swap(++nums[dest],nums[cur]);

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int left=-1,right=left+1;
        int n=nums.size();
        for(;right<n;)
        {
            if(nums[right]!=0)
            {
                swap(nums[++left],nums[right++]);
            }
            else right++;
        }
    }
};

算法总结:

        这个⽅法是往后我们学习「快排算法」的时候,「数据划分」过程的重要⼀步。如果将快排算法拆 解的话,这⼀段⼩代码就是实现快排算法的「核⼼步骤」。

2. 复写零(easy)

1.暴力求解:

两层循环嵌套,第一层找0,第二层while将从0这个位置开始的元素全都往后移动。

class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        int left=0;
        int n=arr.size();
        for(;left<n;left++)
        {
            if(arr[left]==0)
            {
                int right=n-1;
                while(right>left)
                {
                    arr[right]=arr[right-1];
                    right--;
                }
                left++;
            }
        }
    }
};

显然时间复杂度是O(N^2) ,那么就要想办法来进行优化,可以考虑双指针,就能直接降低一维时间复杂度,变成O(N);

2.双指针

1).寻找最后一个进行复写的元素

利用双指针,cur=0,dest=-1,只要nums[cur]==0 dest就走两格;否则 dest走一格;但是无论怎么样cur都走一格。直到dest到达结尾处n-1 停止,即 判断dest>=n-1 处 break;

2).处理边界情况

当dest==n 时,说明nums[cur]此时对应的0,并且dest越界到n处,那么此时nums[n] nums[n-1] 都要复写成0,那么就直接进行修改arr[n-1]=0;cur-=1;dest-=2;

3).从后向前进行复写

在left开始就判断arr[left]==0 如果是,就用right指针,从arr尾部开始,依次把元素往后移,直到right==left停止,这样当left到达n-1时结束

优化:

1.从左往右开始寻找,找到最后一个要被复写的值的下标cur,有两种情况,1.当dest自然在n-1位置停下,这个时候就要判断if(dest>=n-1) 就break;2.当最后cur遇到一个0,让dest++两下直接越界到n处,那么这个时候最后复写的就是0元素,那么n-1处跟n处都要被复写成0,所有,在这种边界条件下就直接进行修改,arr[n-1]=0,cur-=1,dest-=2;在完成正常的复写操作。

2.最后就是复写操作,在arr[cur]==0 的时候arr[dest--]=arr[cur];就复写一次,否则就复写两次,cur都要进行一次-- 边界情况就是cur要>=0,在cur==0时任然完成一次复写

class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        int n=arr.size();
        //1.找到最后一个复写的数
        int cur=0,dest=-1;
        while(cur<n)
        {
            if(arr[cur]) dest++;
            else dest+=2;
            //当dest==n-1位置的时候就说明,dest已经可以结束运动了
            if(dest>=n-1) break;
            cur++;
        }

        //处理边界情况
        if(dest==n)
        {
            arr[n-1]=0;
            cur-=1;
            dest-=2;
        }

        //从后向前完成复写操作
        while(cur>=0)
        {
            if(arr[cur]) arr[dest--]=arr[cur];
            else
            {
                arr[dest--]=0;
                arr[dest--]=0;
            }
            cur--;
        }
    }
};

实际上这题双指针并不好想,所以一定要多总结,这题双指针优化还是挺明显的。

3. 快乐数(medium)

快乐数,实际上就是对于一个数字的每一位进行拆分,平方后相加继续拆分,看到这种拆分确实可以用双指针的思想,一直拆分相加,可以想到,后面会是相当于一个循环链表一样的题目,判断是否纯在循环点,判断最后ret是否等于1,如果是就返回true ; 否则 返回false;

1.简单证明:

a. 经过⼀次变化之后的最⼤值 ⼤ 9 9^2 * 10 = 810 ( 999999999 ),也就是变化的区间在 2^31-1=2147483647 。选⼀个更⼤的最 [1, 810] 之间;

b. 根据「鸽巢原理」,⼀个数变化 811 次之后,必然会形成⼀个循环;

c. 因此,变化的过程最终会⾛到⼀个圈⾥⾯,因此可以⽤「快慢指针」来解决。

解法(快慢指针):

算法思路: 根据上述的题⽬分析,我们可以知道,当重复执⾏ x 的时候,数据会陷⼊到⼀个「循环」之中。 ⽽「快慢指针」有⼀个特性,就是在⼀个圆圈中,快指针总是会追上慢指针的,也就是说他们总会 相遇在⼀个位置上。如果相遇位置的值是 1 ,那么这个数⼀定是快乐数;如果相遇位置不是 1 的话,那么就不是快乐数。

补充知识:如何求⼀个数n每个位置上的数字的平⽅和。

a. 把数 n 每⼀位的数提取出来:

循环迭代下⾯步骤:

i. int t = n % 10 提取个位;

ii. n /= 10 ⼲掉个位; 直到 n 的值变为 0 ;

b. 提取每⼀位的时候,⽤⼀个变量  t mp = tmp + t * t C++算法代码: tmp 记录这⼀位的平⽅与之前提取位数的平⽅和。

class Solution 
{
 public:
    int bitSum(int n) // 返回 n 这个数每⼀位上的平⽅和
{ 
        int sum = 0;
        while(n)
        {
            int t = n % 10;
            sum += t * t;
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n) 
    {
        int slow = n, fast = bitSum(n);
        while(slow != fast)
        {
            slow = bitSum(slow);
            fast = bitSum(bitSum(fast));
        }
        return slow == 1;
    }
 };

2.但是我后来发现,其实只用hash表+字符串来做这题也十分简单

class Solution {
public:
    bool isHappy(int _n) {
        unordered_set<int> hash;
        while(1)
        {
            string s=to_string(_n);
        int n=s.size();
        vector<int> v;
        int ret=0;
        for(int i=0;i<n;i++)
        {
            ret+=pow((s[i]-'0'),2);
        }
        if(hash.count(ret)) return false;
        hash.insert(ret);
        if(ret==1) return true;
        _n=ret;
        }
        return false;
    }
};

判断这个数字每一位平方和是否最后相加等于1,那么就把每次算出的ret都放入hash进行判断,如果存在重复的数字,说明死循环了;否则就是快乐数

4. 盛⽔最多的容器(medium)

这一题是真的经典,很好的说明了双指针的作用。哪怕是花了一天事件弄懂这一题也是值得的。

1.解法⼀(暴⼒求解)(会超时):

算法思路: 枚举出能构成的所有容器,找出其中容积最⼤的值。

◦ 容器容积的计算⽅式: 设两指针 i , j ,分别指向⽔槽板的最左端以及最右端,此时容器的宽度为 容器的⾼度由两板中的短板决定,因此可得容积公式: j - i 。由于 v = (j - i) * min( height[i], height[j])

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

2.双指针,很难想

1.就是暴力,两层循环嵌套,想都不要想肯定超时

2.那么就考虑利用双指针,我们定义left和right,分别从左往右,和从右往左开始遍历,这样整个遍历完,时间复杂度都只是O(N),
有题知道,V=H*W,那么每次移动W宽是必定会减少的,,就有三种情况,

1).H高也减少,V减少,不考虑 .

2).H不变,体积也会减少,不考虑 。

3).H变大,那么就可以从这里下手,那么,我们每次判断就是判断left和right 谁先移动,这样我们就考虑每次移动的指针都是较小的那个,然后计算出体积再次进行比较,如果此时算出的体积>ret 那么就更新ret,直到left==right时停止循环,不在进行计算。会得到最大的ret。

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

5. 有效三⻆形的个数(medium)

这一题确实也很难,但还是那句话,如果花一天事件去学习,那也是值得的。

1.解法⼀(暴⼒求解)(会超时):

算法思路: 三层 for 循环枚举出所有的三元组,并且判断是否能构成三⻆形。 虽然说是暴⼒求解,但是还是想优化⼀下: 判断三⻆形的优化:

▪ 如果能构成三⻆形,需要满⾜任意两边之和要⼤于第三边。但是实际上只需让较⼩的两条边 之和⼤于第三边即可。

▪ 因此我们可以先将原数组排序,然后从⼩到⼤枚举三元组,⼀⽅⾯省去枚举的数量,另⼀⽅ ⾯⽅便判断是否能构成三⻆形。

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

2.很难,很有意义的双指针

1.第一步肯定就是暴力,但是如果想到暴力求解每次都枚举每一种情况的话,就要用i,j,k三种来嵌套循环,是O(N^3);绝对会超时

2.那么就要来考虑优化,本来是双指针专题,一直都想不到好的解决办法,不管是把left放在0 right=1,来枚举第三个数,就又变成了暴力,那么这种肯定不可取。

3.对于确定能构成三角形,有一个小demo 就是能确保a<=b<=c 的情况下,a+b>c,那么在剩下的判断中,无论是a+c 还是 b+c 那都是妥妥的大于第三个数,因为里面c最大,那么我们只需要判断,在有序的a<b<c 里面,a+b>c成立,就可以判断能构成三角形。

4.那么此时,为了得到比较大的优化程度,我们设left=0,end=n-1,right=end-1,将end固定到最大值的位置,right=end-1处,来进行判断,使left++来进行移动,只要有满足nums[left]+nums[right]>nums[end]的值,就可以确保,left 至 right 之间的所有值 都能加上nums[right] > nums[end] 那么就可以一次性算出 这里面满足条件的个数ret+=right-left; 这是 a+b>c的情况

5.在计算出left 到 right 满足条件的次数之后,那么right--,来缩小范围,如果不能满足,就left++,直到满足或left==right为止,重复上面步骤,就能够得到所有关于end对应元素的所有情况,那么此时在while外end--,更新end right 和 left 重复以上情况,直到 最后只剩下三个元素进行比较,完成所有情况的遍历,这种时间复杂度也是质的飞跃,:快排O(NlogN+N^2) 比 O(N^3) 快很多很多倍

class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        int n=nums.size();
        int ret=0;
        int left=0,end=n,right=end-1;
        for(int i=0;i<=n-3;i++)
        {
            end--;
            right=end-1;
            left=0;
            while(left<right)
            {
                if(nums[left]+nums[right]>nums[end])
                {
                    ret+=right-left;
                    right--;
                }
                else left++;
            }
        }
        return ret;
    }
};

6、LCR 179.查找总价格为目标值的两个商品

还是比较简单的,很容易想到的双指针,就是left++,right-- 找趋近的值

1.暴力:(超时)

算法思路: 两层 for 循环列出所有两个数字的组合,判断是否等于⽬标值。

算法流程: 两层 for 循环: ◦ 外层 for 循环依次枚举第⼀个数 a ;

◦ 内层 for 循环依次枚举第⼆个数 b ,让它与 a 匹配; ps :这⾥有个魔⻤细节:我们挑选第⼆个数的时候,可以不从第⼀个数开始选,因为 a 前 ⾯的数我们都已经在之前考虑过了;因此,我们可以从 a 往后的数开始列举。

◦ 然后将挑选的两个数相加,判断是否符合⽬标值。

2.双指针(缩小范围):

总结:1.跟前面的双指针一样,left=0,right=n-1,这样就可以保证在nums[left] + nums[right] 这个范围里面进行调整,如果小于target ,left++;如果大于target,right--。这样不断地缩小关于target的范围,进行调整

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

7. 三数之和(medium)

重头戏,还是那句话,花一天时间学会这一天,双指针基本也没啥问题了,有很多细节需要注意。

通过第6题,可以理解为双数之和,这里的三数之和,只不过是多了一个固定的i而已。

特别锻炼代码能力!!!

1.暴力:想都不用想 O(N^3) 绝对超时;

2.排序+双指针

如果数组有必要排序,就排序,数组一旦有序,就要想二分或者双指针,但是能用双指针就不用二分,因为双指针就能直接降调一维时间复杂度。

1.这题仍然是固定的找三数问题,那么绝对是固定一个指针,让另外两个指针,left=i+1,right=n-1;首先就考虑两个问题,

1.是考虑在left 跟 right 区间进行查找的时候,找到了b=nums[left]+nums[right];=-nums[i] 的结果时,不能退出,仍然要继续进行查找,那么就要保证left++,right--; 对于int a=nums[i]; 的设定,就要考虑left right 指针的移动,当-a>b 的时候b就太小了,left++ ;当-a<b 的时候就是b太大了 要right--;这样做到不漏数据

2.做到去重数据:
1.利用unordered_set<vector<int>> hash 进行去重,但是这样过于无脑,不用考虑left和right跳过重复元素,i跳过重复元素的情况


2.每次在找到有满足数据的left 和 right 时,只是将所有数据都存入v里面,考虑left right i 跳过重复数据 就又能有常数级的优化(小demo) 跳过重复元素 就left++ right-- 那么在这之前就要考虑while(left<right&&nums[left]==nums[left+1]) left++;
                    while(right>left&&nums[right]==nums[right-1]) right--; 
致命的问题就是对于left<right 防止当数组里面数据为全0 的时候 left 和 right 就都会越界,对于i while(i<n-1&&nums[i]==nums[i+1]) i++; 进行去重的时候,要注意边界条件,i<n-1 防止i越界

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        int n=nums.size();
        sort(nums.begin(),nums.end());
        vector<vector<int>> ret;
        for(int i=0;i<n-1;i++)
        {
            if(nums[i]>0) break;
            
            int left=i+1,right=n-1;
            int a=nums[i];
            while(left<right)
            {
                vector<int> v;
                int b=nums[left]+nums[right];
                if(a==-b)
                {
                    v.push_back(nums[i]);
                    v.push_back(nums[left]);
                    v.push_back(nums[right]);
                    //对left 和 right 去重
                    while(left<right&&nums[left]==nums[left+1]) left++;
                    while(right>left&&nums[right]==nums[right-1]) right--; 
                    left++,right--;
                    ret.push_back(v);
                }
                else if(-a>b) left++;
                else right--;
            }
            // 对i去重
            while(i<n-1&&nums[i]==nums[i+1]) i++;
        }
        return ret;
    }
};


 

8.四数之和(medium)

如果真的学会了三数之和,那么真的希望你能自己动手去画图调试四数之和,来独立完成这一题,只不过多了一个j来多固定一个数,跟三数之和完全就是一模一样!

特别锻炼代码能力!!!

双指针:

四数之和跟三数之和一样,就只是要考虑a=target-b 和开 long long 不然会爆数据

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        int n=nums.size();
        sort(nums.begin(),nums.end());
        vector<vector<int>> ret;
        for(int i=0;i<=n-4;i++)
        {
            for(int j=i+1;j<=n-3;j++)
            {
                long long a=nums[i]+nums[j];
                int left=j+1,right=n-1;
                while(left<right)
                {
                    long long b=nums[left]+nums[right];
                    if(a==target-b)
                    {
                        ret.push_back({nums[i],nums[j],nums[left],nums[right]});
                        while(left<right&&nums[left]==nums[left+1]) left++;
                        while(left<right&&nums[right]==nums[right+-1]) right--;
                        left++,right--;
                    }
                    else if(a<target-b) left++;
                    else right--; 
                }
                while(j<=n-3&&nums[j]==nums[j+1]) j++;
            }
            while(i<=n-4&&nums[i]==nums[i+1]) i++;
        }

        return ret;
    }
};

我觉得对我自己是有很大提升的,希望对你也有帮助!~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值