• 算法之双指针


    双指针

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

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

    快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。
    这种⽅法对于处理环形链表或数组⾮常有⽤。
    其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。
    快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
    在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢。


    题目1: 283. 移动零 - 力扣(LeetCode)

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

    解法(快排的思想: 数组划分区间-数组分两块)

    思路:

    在本题中,我们可以⽤⼀个cur指针来扫描整个数组,另⼀个 dest 指针⽤来记录⾮零数序列的最后⼀个位置。根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。在 cur 遍历期间,使 [0, dest] 的元素全部都是⾮零元素, [dest + 1,cur - 1] 的元素全是零, [cur,n-1]的元素是待处理的元素


    算法流程:

    a. 初始化 cur = 0 (⽤来遍历数组),dest = -1(指向⾮零元素序列的最后⼀个位置。
    因为刚开始我们不知道最后⼀个⾮零元素在什么位置,因此初始化为 -1)


    b. cur 依次往后遍历每个元素,遍历到的元素会有下⾯两种情况:
    遇到的元素是0,cur 直接 ++ . 因为我们的⽬标是让 [dest+1, cur-1] 内的元素全都是零, 因此当cur遇到0的时候, 直接 ++ , 就可以让 0 在 cur - 1 的位置上, 从⽽在 [dest + 1, cur - 1] 内;
    2. 遇到的元素不是0,dest++ ,并且交换cur位置和dest位置的元素,之后让cur++ ,扫描下⼀个元素。


    因为 dest 指向的位置是⾮零元素区间的最后⼀个位置, 如果扫描到⼀个新的⾮零元
    素, 那么它的位置应该在dest + 1 的位置上, 因此 dest 先⾃增1.
    dest++ 之后,指向的元素是0元素或者也可能是cur, 因为当dest == cur时,0区间不存在, 因此可以交换dest和cur位置上的数据(如果此时dest==cur,相当于自我交换,可以加if判断省略掉),实现 [0, dest] 的元素全部都是⾮零元素, [dest + 1, cur - 1] 的元素全是零。

    1. class Solution {
    2. public:
    3. void moveZeroes(vector<int>& nums) {
    4. int dest = -1;
    5. int cur = 0;
    6. while(cur != nums.size())
    7. {
    8. if(nums[cur] == 0)
    9. cur++;
    10. else
    11. {
    12. if(cur != dest+1)
    13. swap(nums[dest+1],nums[cur]);
    14. dest++;
    15. cur++;
    16. }
    17. }
    18. }
    19. };
    1. class Solution {
    2. public:
    3. void moveZeroes(vector<int>& nums) {
    4. for(int cur = 0, dest = -1; cur < nums.size();cur++)
    5. if(nums[cur])
    6. if(++dest != cur)
    7. swap(nums[dest],nums[cur]);
    8. }
    9. };

    题目2: 1089. 复写零 - 力扣(LeetCode)

    先根据"异地"操作, 然后优化成双指针下的"就地"操作

    然后开始就地操作: 

    如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆盖掉」, 比如这里的2就先被覆盖掉了, 往后所有值就都是0了.

    所以可以尝试「从后向前」, 但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两步:
    i. 先找到最后⼀个复写的数;
    ii. 然后从后向前进⾏复写操作。 

    以此类推: 

    算法流程:
    a. 初始化两个指针 cur = 0 , dest = -1 :
    b. 找到最后⼀个复写的数:

    i. 当 cur < n 的时候,⼀直执⾏下⾯循环:
    • 判断 cur 位置的元素:
            ◦ 如果是 0 的话, dest 往后移动两位;
            ◦ 否则, dest 往后移动⼀位。
    • 判断 dest 时候已经到结束位置,如果结束就终⽌循环;
    • 如果没有结束, cur++ ,继续判断。

    c. 判断 dest 是否越界到n的位置:

    i. 如果越界, 执⾏下⾯三步:
                1. n - 1 位置的值修改成 0 ;
                2. cur 向前移动⼀步;
                3. dest 向前移动两步。

    d. 从 cur 位置开始往前遍历原数组,依次还原出复写后的结果数组:

    i. 判断 cur 位置的值:
            1. 如果是 0 : dest以及dest - 1 位置修改成 0 , dest -= 2 ;
            2. 如果⾮零: dest 位置修改成? 0 , dest -= 1 ;
    ii. cur-- ,复写下⼀个位置。 

    1. class Solution {
    2. public:
    3. void duplicateZeros(vector<int>& arr)
    4. {
    5. // 1. 先找到最后⼀个数
    6. int dest = -1, cur = 0, n = arr.size();
    7. while(cur < n)
    8. {
    9. if(arr[cur]) dest++;
    10. else dest += 2;
    11. if(dest >= n-1) break;
    12. cur++;
    13. }
    14. // 2. 处理⼀下边界情况
    15. if(dest == n)
    16. {
    17. arr[n-1] = 0;
    18. cur--;
    19. dest-=2;
    20. }
    21. // 3. 从后向前完成复写操作
    22. while(cur >= 0)
    23. {
    24. if(arr[cur]) arr[dest--] = arr[cur--];
    25. else
    26. {
    27. arr[dest--]=0;
    28. arr[dest--]=0;
    29. cur--;
    30. }
    31. }
    32. }
    33. };

    题目3: 202. 快乐数 - 力扣(LeetCode)

    将 对于⼀个正整数,每⼀次将该数替换为它每个位置上的数字的平⽅和 这个操作记为f操作;
    题⽬提示我们, 当我们不断重复f操作的时候, 计算⼀定会死循环,死循环的⽅式有两种:

    情况⼀:⼀直在1中死循环,即 1 -> 1 -> 1 -> 1...... 
    情况⼆:在历史的数据中死循环,但始终变不到1, 由于上述两种情况只会出现⼀种, 因此, 只要我们能确定循环是在「情况⼀」中进⾏,还是在「情况⼆」中进⾏, 就能得到结果

    用鸽巢原理简单证明:

    鸽巢原理: n个巢和n+1个鸽子, 至少有一个巢的里面的鸽子数大于等于1

    此题中n是int类型, 最大值是 2^31 - 1 约等于2.1 × 10^9, 不妨假设一个数9.9 × 10^9, 这个数经过f变换后形成的数一定是要大于int最大数f变换后的数的, 也就是说int里的所有数经过f变换后形成的数一定是小于9.9 × 10^9 f 变换后的那个数也就是810, 所以2.1 × 10^9 f变换后的取值范围是[1, 810], 现在取出int里的随机一个数, 对它进行811次f变换, 假设它运气很好810次变换后的数都是不一样的, 但第811次变换的数一定是与前面某个数相同的, 所以一定会进行一个循环

    1. class Solution {
    2. public:
    3. int bitsum(int n)
    4. {
    5. int sum = 0;
    6. while(n)
    7. {
    8. int tmp = n % 10;
    9. sum += tmp*tmp;
    10. n /= 10;
    11. }
    12. return sum;
    13. }
    14. bool isHappy(int n)
    15. {
    16. int slow = n, fast = bitsum(n);
    17. while(slow != fast)
    18. {
    19. slow = bitsum(slow);
    20. fast = bitsum(bitsum(fast));
    21. }
    22. if(slow == 1)
    23. return true;
    24. else
    25. return false;
    26. }
    27. };

    题目4:11. 盛最多水的容器 - 力扣(LeetCode)

     解法⼀:暴力求解, 会超时.

    容器容积的计算⽅式: 

    设容器左端下标为left, 右端下标为right

    宽度w = ( right - left )

    容器高度 h = min(height[left],height[right]). 

    容器体积V = w * h


    依次从左到右把每一种组合对应的容器的体积求出来, 求出最大值, 时间复杂度为O(N^2), 会超时. 

     解法⼆(对撞指针):

    设两个指针left,right 分别指向容器的左右两个端点,此时容器的容积v = (right - left) * min( height[right], height[left]).
    如果此时我们固定⼀个边界, 改变另⼀个边界, ⽔的容积会有如下变化形式:

    1. 容器的宽度⼀定变⼩, 因为此时已经是边界, 指针要向里挪动进行枚举, 向里挪动宽度一定减小.

    2. 由于较⼩边界对应的高度, 决定了⽔的⾼度, 假如较小的边界为left, 此时固定left, 移动right,right向左移动, 如果遇到小于left的高度, 体积一定比最开始的要小, 因为V  = w * h, w和h都变小, 如果遇到大于等于left的高度, 体积也是比最开始的要小, 因为h最高也就是left对应的h, w在减小, 所以体积会减小, 所以可以发现较小边界对应的高度无论和谁组合都是小于最开始的高度的,较小边界和其余的组合情况都可以舍去, left就可以++跳过这个高度了(如果right对应的高度小那就right--), 然后继续重复上述的过程, 要注意每次都要先计算出挪动前的体积,不断更新max,直到left和right相遇结束.

    1. class Solution {
    2. public:
    3. int maxArea(vector<int>& height)
    4. {
    5. int left = 0;
    6. int right = height.size()-1;
    7. int max = 0;
    8. int tempv;
    9. while(left < right)
    10. {
    11. int min = height[left] < height[right] ? height[left] : height[right];
    12. tempv = (right-left) * min;
    13. max = tempv > max ? tempv : max;
    14. if(min == height[left])
    15. left++;
    16. else if(min == height[right])
    17. right--;
    18. }
    19. return max;
    20. }
    21. };

    题目5:611. 有效三角形的个数 - 力扣(LeetCode)

    解法⼀(暴⼒求解):

    三层for循环枚举出所有的三元组, 并且判断是否能构成三⻆形, 虽然说是暴⼒求解, 但是还是想优化⼀下, 如果能构成三⻆形,需要满⾜任意两边之和要⼤于第三边, 但是实际上只需让较⼩的两条边之和⼤于第三边即可, 因此我们可以先将原数组排序, 然后从⼩到⼤枚举三元组, ⼀⽅⾯省去枚举的数量,另⼀⽅⾯⽅便判断是否能构成三⻆形。

    伪代码:

    1. for(int i = 0; i < N ; i++)
    2. for(int j = i + 1; j < N; j++)
    3. for(int k = j+1; k < N; k++)
    4. {
    5. //判断三角形
    6. }

    这样的时间复杂度是O(N^3), 具体来说是: 3*N^3(因为要判断3次), 但加了优化后就变成了 N*logN + N^3, (一次排序,一次判断).

    1. class Solution {
    2. public:
    3. int triangleNumber(vector<int>& nums) {
    4. // 1. 排序
    5. sort(nums.begin(), nums.end());
    6. int n = nums.size(), ret = 0;
    7. // 2. 从⼩到⼤枚举所有的三元组
    8. for (int i = 0; i < n; i++) {
    9. for (int j = i + 1; j < n; j++) {
    10. for (int k = j + 1; k < n; k++) {
    11. // 当最⼩的两个边之和⼤于第三边的时候,统计答案
    12. if (nums[i] + nums[j] > nums[k])
    13. ret++;
    14. }
    15. }
    16. }
    17. return ret;
    18. }
    19. };

     解法⼆(排序+双指针):

    先将数组排序, 根据 解法⼀ 中的优化思想, 我们可以固定⼀个最⻓边, 然后在⽐这条边⼩的有序数组中找出⼀个⼆元组, 使这个⼆元组之和⼤于这个最⻓边, 由于数组是有序的, 我们可以利⽤ 对撞指针 来优化.
    设最⻓边位置的位置maxlength, left = 0, right = maxlength - 1, 区间[left, right] 是maxlength位置左边的区间(也就是⽐它⼩的区间).

    1.如果 nums[left] + nums[right] > nums[maxlength] ,说明[left,right-1]区间上的所有数, 都能和right,maxlength构成三角形, 因为left是最小的数, 最小的都能构成, 区间内比left大的也能构成,所以满⾜条件的有 right - left 种, 此时 right 位置的元素的所有情况相当于全部考虑完毕, right-- ,进⼊下⼀轮判断.

    2. nums[left] + nums[right] <= nums[maxlength], 说明区间[left+1, right]上的所有数都不能和right,maxlength构成三角形, right是最大的数, 最大的都不能构成, 区间内比right小的也不能构成, 所以left位置的元素可以舍去, left++,进⼊下⼀轮判断.

    3. 重复上述步骤直到这一轮判断结束, maxlength这个位置的所有情况都判断完, 然后maxlength--, 向左移动, 继续判断新的一大轮.

    1. class Solution {
    2. public:
    3. int triangleNumber(vector<int>& nums)
    4. {
    5. //优化
    6. sort(nums.begin(),nums.end());
    7. //用双指针解决问题
    8. int ret = 0;
    9. int maxlength = nums.size()-1;//先固定最大的数
    10. int left = 0, right = maxlength - 1;//找好左右区间
    11. //利用双指针的思路进行判断
    12. while(maxlength >= 2)
    13. {
    14. while(left < right)
    15. {
    16. if(nums[left] + nums[right] > nums[maxlength])
    17. {
    18. ret += (right - left);
    19. right--;
    20. }
    21. else
    22. {
    23. left++;
    24. }
    25. }
    26. //一轮判断结束,重新设置各个指针的位置
    27. maxlength--;
    28. left = 0, right = maxlength - 1;
    29. }
    30. return ret;
    31. }
    32. };

    题目6: LCR 179. 查找总价格为目标值的两个商品 - 力扣(LeetCode)

      解法⼀(暴⼒解法):

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

    1. for(int i = 0; i < N; i++)
    2. for(int j = i+1; j < N; j++)
    3. {
    4. //判断..
    5. }

    解法⼆(双指针 + 对撞指针):

    注意到本题是升序的数组, 因此可以⽤ 对撞指针 优化时间复杂度. 

    思路和上一题类似, 初始化 left,right 分别指向数组的左右两端, 当left < right 的时候,⼀直循环进行判断:

    1. 当nums[left] + nums[right] == target时, 说明找到结果, 记录结果并返回.

    2. 当nums[left] + nums[right] < target 时, 因为数组是有序的, 区间[left+1,right]内的数和left相加都小于target, left可以直接++跳过

    3. 当nums[left] + nums[right] > target 时, 因为数组是有序的, 区间[left,right-1]内的数和right相加都大于target, right可以直接--跳过.

    然后重复判断直到找到为止.

    1. class Solution {
    2. public:
    3. vector<int> twoSum(vector<int>& price, int target)
    4. {
    5. int left = 0, right = price.size()-1;
    6. vector<int> ret;
    7. while(left < right)
    8. {
    9. if(price[left] + price[right] == target)
    10. {
    11. ret.push_back(price[left]);
    12. ret.push_back(price[right]);
    13. return ret;
    14. }
    15. else if(price[left] + price[right] > target)
    16. right--;
    17. else
    18. left++;
    19. }
    20. return ret;
    21. }
    22. };

    题目7:15. 三数之和 - 力扣(LeetCode)

     解法(排序+双指针):

    本题与两数之和类似, 与两数之和稍微不同的是, 题⽬中要求找到所有不重复的三元组, 那我们可以利⽤在两数之和的双指针思想, 来对我们的暴⼒枚举做优化:
    1. 先排序
    2. 然后固定⼀个数target
    3. 在这个数后⾯的区间内, 使⽤双指针算法快速找到两个数之和等于-target即可, 但是要注意的是需要去重.

    去重:
            1. 找到⼀个结果之后, left和right指针要跳过重复的元素.
            2. 当使⽤完⼀轮查找之后, 固定的target也要跳过重复的元素.

    1. class Solution {
    2. public:
    3. vectorint>> threeSum(vector<int>& nums)
    4. {
    5. sort(nums.begin(),nums.end());//排序
    6. vectorint>> ret;
    7. //第一次先把第三个数定为最右端的数
    8. int i = nums.size()-1;
    9. int left = 0, right = i - 1;
    10. while(i >= 2)
    11. {
    12. while(left < right)
    13. {
    14. //思路和之前一样,left+right小代表left的任意组合都小,直接跳过
    15. if(nums[left] + nums[right] < -nums[i])
    16. left++;
    17. //left+right大代表right的任意组合都大,直接跳过
    18. else if(nums[left] + nums[right] > -nums[i])
    19. right--;
    20. //找到了
    21. else
    22. {
    23. ret.push_back({nums[left],nums[right],nums[i]});
    24. //去重left和right, 和刚才left right相等的直接跳过,但要保证不越界
    25. while(nums[left++] == nums[left] && right > left);
    26. while(nums[right--] == nums[right] && right > left);
    27. }
    28. }
    29. if(nums[i] < 0)//常数级别的小优化, 如果nums[i]小于0,前面无法再找到相加等于0的数,因为是有序的
    30. break;
    31. //去重i,更新left和right
    32. while(nums[i--] == nums[i] && i>=2);
    33. left = 0;
    34. right = i - 1;
    35. }
    36. return ret;
    37. }
    38. };

    题目8: 18. 四数之和 - 力扣(LeetCode) 

    思路:(排序 + 双指针)

    算法思路:

    思路和三数之和类似, 都是先固定一个数然后用双指针算法去寻找, 不过是多嵌套了一层循环去固定第三个数.

    a. 依次固定⼀个数forth.
    b. 在这个数的后⾯区间上, 利⽤三数之和找到三个数, 使这三个数的和等于target - a 即可.

    1. class Solution {
    2. public:
    3. vectorint>> fourSum(vector<int>& nums, int target)
    4. {
    5. vectorint>> ret;
    6. //先排序
    7. sort(nums.begin(),nums.end());
    8. int forthi = nums.size() - 1;
    9. while(forthi >= 3)
    10. {
    11. long long target2 = target - nums[forthi];//固定三数之和的数
    12. int thirdi = forthi - 1;
    13. int left = 0, right = thirdi - 1;
    14. while(thirdi >= 2)
    15. {
    16. long long target3 = target2 - nums[thirdi];//固定两数之和的数
    17. while(left < right)
    18. {
    19. if(nums[left] + nums[right] < target3)
    20. left++;
    21. else if(nums[left] + nums[right] > target3)
    22. right--;
    23. else
    24. {
    25. ret.push_back({nums[left],nums[right],nums[thirdi],nums[forthi]});
    26. //去重
    27. while(nums[left++] == nums[left] && left < right);
    28. while(nums[right--] == nums[right] && left < right);
    29. }
    30. }
    31. //去重
    32. while(nums[thirdi--] == nums[thirdi] && thirdi >=2);
    33. left = 0;
    34. right = thirdi - 1;
    35. }
    36. //去重
    37. while(nums[forthi--] == nums[forthi] && forthi >=3);
    38. }
    39. return ret;
    40. }
    41. };

    注意target用long long存储, 测试用例里有int不能表示的数.

  • 相关阅读:
    RabbitMQ Client (C/C++)
    从苏宁电器到卡巴斯基(第二部)第33篇:我当高校教师的这几年 IX
    总结:OpenStack笔记
    怎么改变placeholder提示字的颜色用CSS
    ​中秋团圆季《乡村振兴战略下传统村落文化旅游设计》许少辉八月新书
    什么是AIoT?
    Exception | ShardingSphere | ShardingSphere引发的IndexOutOfBoundsException
    【Docker】本地镜像与私有库:发布、拉取,图文展示全过程
    Ansible自动化部署工具-组件及语法介绍
    详细图解 Netty Reactor 启动全流程 | 万字长文 | 多图预警
  • 原文地址:https://blog.csdn.net/ZZY5707/article/details/134452995