双指针算法:高效解决问题的利器

目录

概念:

双指针的常见应用场景和实现方式:

实战例题(深度刨析每一道经典例题)

leetcode283例题

leetcode1089例题

leetcode202例题

leetcode11例题

leetcode611例题

leetcode15例题

leetcode18例题

双指针的理解

概念:

双指针算法是一种常用的编程技巧,核心思想是使用两个指针(或索引)在数据结构(通常是数组或链表)中移动,通过合理调整指针位置来高效解决问题。它的主要优势是能将原本需要嵌套循环(时间复杂度 O (n²))的问题优化为线性时间复杂度(O (n)),从而大幅提升效率。

双指针的常见应用场景和实现方式:

  1. 相向指针:两个指针从数组两端向中间移动

    • 典型应用:二分查找、两数之和(有序数组)、判断回文串等
    • 工作方式:根据条件判断移动左指针(增大)或右指针(减小)
  2. 同向指针:两个指针从同一端出发,向相同方向移动

    • 典型应用:滑动窗口问题、移除重复元素、寻找最长子串等
    • 工作方式:快指针先行探索,慢指针负责记录有效位置
  3. 分离指针:两个指针分别处理不同的序列

    • 典型应用:合并两个有序数组、判断链表是否相交等
    • 工作方式:分别遍历两个序列,根据条件移动相应指针

双指针算法的关键在于找到指针移动的规则,确保每个元素最多被访问一次。这种技巧特别适合处理有序数据结构,能在不使用额外空间(或仅使用常数空间)的情况下解决问题,兼顾了时间和空间效率。

例如,在有序数组中寻找两数之和等于目标值的问题:

  • 左指针初始在数组开头,右指针在数组末尾
  • 如果两数之和等于目标值,找到答案
  • 如果和小于目标值,左指针右移(增大总和)
  • 如果和大于目标值,右指针左移(减小总和)
  • 直到找到答案或指针相遇

这种方法只需一次遍历即可完成,时间复杂度为 O (n),远优于暴力解法的 O (n²)。

实战例题(深度刨析每一道经典例题)

leetcode283例题

算法原理讲解:

注意遇到这种题目,读完之后发现他是给一个区域,也就是数组进行分块

所以这类题可以叫数组分块或者数组划分,一边是非0数字,一边是0数字

非0数字还要保证其相对顺序没有发生改变

可以使用双指针:cur从前往后扫描,des标识要交换的第一个0的位置

也就是cur去找非0元素,des去标记0元素(两个指针都是从前往后)

cur从前往后找的时候:1.遇到0元素,cur++    2.遇到!0元素和des位置(des始终会指向一个0元素)的元素交换

知道cur遍历完整个数组就算结束了

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int size=nums.size();
        int cur=0;
        int des=-1;
        while(cur<size){
            if(nums[cur]){
                des++;
                int tmp;
                tmp=nums[des];
                nums[des]=nums[cur];
                nums[cur]=tmp;
            }
                cur++;
        }
    }
};

leetcode1089例题

算法原理讲解:

如果能够开辟额外的数组这道题是一道简单题,难点就在只能在原来数组上操作

可以先进行”异地操作“:也就是如果在开辟一个数组出来用两个指针能否完成

在进行”本地“迁移操作:是否能够在原先数组上通过双指针完成

发现:两个指针两个数组很简单,但是对于一个数组会从左往后一起走会出现问题

问题:des指针可能会走到cur指针的前面,也就是des指针会走的快,然后覆盖掉要检查的元素

解决方案:可以从右向左尝试,发现如果对于示例,只要知道最后一个元素应该填4,des指向最后一个位置,然后cur指向4,从右往左一起走,des指针就不会超过cur指针

这里的特殊情况就是如果倒数第二个数字是0的话,des可能就会超出原本数组的边界

所以要处理一下特殊情况

本道例题关键在于:des不能走到cur的前面,覆盖掉要检查的元素,之前283题不会覆盖一直都是cur走在最前面检查元素

class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        int n = arr.size();
        int cur = 0;
        int des = -1;
        
        // 1. 找到最后一个需要被处理的元素
        while (cur < n) {
            if (arr[cur] == 0) {
                des += 2;
            } else {
                des += 1;
            }
            
            // 当des超出范围时停止
            if (des >= n - 1) {
                break;
            }
            cur++;
        }
        
        // 2. 处理特殊情况:最后一个元素是0且刚好好超出数组
        if (des == n) {
            arr[n - 1] = 0;
            des -= 2;
            cur--;
        }
        
        // 3. 从后向前复制元素
        while (cur >= 0 && des >= 0) {
            if (arr[cur] == 0) {
                arr[des] = 0;
                des--;
                arr[des] = 0;
            } else {
                arr[des] = arr[cur];
            }
            des--;
            cur--;
        }
    }
};

leetcode202例题

算法原理讲解:

可以先自行举一下19这个快乐数,和2这个非快乐数

发现19这个快乐数最后变成1,然后一直以1循环

然后2这个快乐数会一直计算下去,最后会计算会4

对此我们可以抽象出来

如果是快乐数,那环内就会一直是1,如果非快乐数,那环内就不会是1

对此我们可以联想到数据结构中的链表:在判断一个链表是否成环的时候,我们当时使用了快慢指针的思路:快指针走两步,慢指针走一步,如果慢指针和快指针相遇了,说明有环

这里我们只需要结合快慢指针,并且判断环内相遇的时候的元素是否为1就行

这里是必定成环的,所以只要判断环内元素是几就行

注意:我们这节讲的双指针法不要局限于平时所学的*之类的,指针指针就是能够通过里面的地址找到元素就行,这里我们可以把每个元素本身定义为指针,例如fast一开始指向19,走两步fast值就变成68,slow就是82,所以我们每次更新fast和slow这个int类型的值就行,直到他们相同的时候判断一下相同的时候是1还是!1

这里扩展一下为什么一定成环

这里题目是有要求的有一个最大数,就是int=2的31次方-1,这里我们假设最大数为999

那9^2+9^2+9^2=243,也就是你的范围是【1,243】,你只要在999这个范围之内变换一定是会回到1-243的,假设你经过244次变换,前面243次每次都是1-243范围内某个值,但是在经过一次变换就会导致某个值中两次,所以就会成环

class Solution {
public:
    bool isHappy(int n) {
        int slow = n;
        int fast = n;
        int fastsum = 0;
        int slowsum = 0;
        // 快指针走两步,慢指针走一步,走完之后判断
        while (1) {
            while (fast) {
                int first = fast % 10;
                fastsum += first * first;
                fast = fast / 10;
            }
            fast = fastsum;
            fastsum = 0;
            while (fast) {
                int first = fast % 10;
                fastsum += first * first;
                fast = fast / 10;
            }
            fast = fastsum;
            fastsum = 0;
            while (slow) {
                int first = slow % 10;
                slowsum += first * first;
                slow = slow / 10;
            }
            slow = slowsum;
            slowsum = 0;
            if (slow == fast) {
                if (slow == 1) {
                    return true;
                } else
                    return false;
            }
        }
    }
};

leetcode11例题

算法原理讲解:

方法一:暴力求解

固定左边指针,右指针依次去遍历,然后记录下以左边固定右边移动的容积最大值

左边++,然后再依次移动右边,又记录最大值

所有的最大值再求一个最大值就可以得出来了

两个for循环就可以搞定,但是这个暴力求解会超时,时间复杂度为N^2

方法二:利用单调性求解

我们先看一个小区间比如6 2 5 4

如果是6和4分别做高,求出一个v1,这个v1=高*宽,宽=3-0=3,高=4,所以v1=12

但是如果是4和5呢?发现宽度减小,并且高度不变,明显这种选法低于v1

如果是4和2呢?宽度减小,高度减小,变成2,明显低于v1

所以下一步我们可以考虑6 2 5这个区间,直接排除掉4作为高这个区间,依次算出每个区间的v

回到题目,我们可以1和7先算一个v1,然后哪边矮删掉哪边,也就是左指针++

8和7做高算v2,右边矮所以右节点--

依次算出每个v,然后更新vmax就行了

class Solution {
public:
    int maxArea(vector<int>& height) {
        int vmax=0;
        int left=0;
        int right=height.size()-1;
        while(left<right){
            //1.先计算v
            //1.1计算宽度
            int weigh=right-left;
            //1.2选择高度,矮的作为高度
            int high=0;
            if(height[left]>height[right])
            high=height[right];
            else
            high=height[left];
            vmax=max(vmax,high*weigh);
            //2.移动指针,哪边小移动哪边
            if(height[left]>height[right])
            right--;
            else
            left++;
        }
        return vmax;
    }
};

leetcode611例题

算法原理:根据题意,判断三个数是否能够构成三角形,我们只需要使最小的两个数a+b>c判断

由于示例说明给的数组不是一个排好序的数组,所以我们可以给数组排序,能够大大优化

第一种解法:暴力枚举

//伪代码
for(int i=0;i<n;i++)
 for(int j=i+1;j<n;j++)
   for(int k=j+1;k<n;k++)
      check(i,j,k);

三层for循环枚举所有的三个数,如果数组没有经过排序,时间复杂度就是O(3*N^3),因为check里面需要比较三次,如果排好序的只要比较i+j>k即可

如果经过排序,时间复杂度就是O(NlogN+N^3),很明显时间优化对比上排序的更优化

第二种解法:利用单调性双指针法

假设数组(2,2,3,4,5,9,10)

先用一个指针p固定10,然后left指针指向2,right指针指向9

判断left+right>p?

有两种情况:

1.left+right>p,此时说明区间最小的和最大的相加>p,那你所有的left++,固定right都是>p的

   所以此时我们只要算出个数也就是下标相减,5-0=5,说明这个区间有5个三角形

2.left+right<=p,此时说明区间最小的和最大的相加小于等于p,构不成三角形,那你所有的固定left,right--都是<=p的

所以针对两种情况,我们只需要第一种操作完right--,第二种操作完left++,不断地缩小区间,这样我们就能够固定p指针,算出以p为其中最大数的三角形

时间复杂度:固定p指针,遍历就是n次,然后left和right分别从两边走到中间,也就是遍历数组一遍,所以n里面套个n,那就是O(N^2)

class Solution {
public:
    int triangleNumber(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int sum = 0;
        int p = nums.size() - 1;
        for (; p >= 2; p--) {
            int left = 0;
            int right = p - 1;

            while (left < right) {
                // left+right>p
                if (nums[left] + nums[right] > nums[p]) {
                    sum += right - left;
                    right--;
                } else {
                    left++;
                }
            }
        }
        return sum;
    }
};

leetcode15例题

算法原理讲解:

第一种解法:暴力枚举,排序+暴力+去重(可以使用c++中的unordered set)

时间复杂度为O(N^3)

第二种解法:双指针

排序+双指针+去重

假设数组为如图上述

先用指针i固定左边,-4,然后再剩下的区间当中找和为4的两个数

比如固定-4,left指向-4,right指向6,-4+6=2<4,说明left要++,-1+6=5>4,right--

-1+5=4,这一组符合,所以-4,-1,5就是一组

当寻找完一组之后不能停,left++,right--同时移动缩小区间

这就能够在区间当中找到所有符合的三数之和等于0

注意去重可以使用容器,但是面试当中如果问还有别的解法我们也要会

去重:

找到一种结果之后,left++,right--的过程当中要跳过和之前一样的元素

当使用完指针i之后也要跳过重复的元素,否则比如你使用了-4,下一个还是-4就要跳过

还有优化的空间,比如i指向一个正数,就没必要在后面找了,正数+正数+正数是不可能等于0的

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> v;
        sort(nums.begin(), nums.end());
        int i = 0;
        int reti = 100;
        for (; i < nums.size(); i++) {
            if (nums[i] > 0)
                break;
            int a = nums[i];
            int left = i + 1;
            int right = nums.size() - 1;
            while (left < right) {
                if (nums[left] + nums[right] + a > 0) {
                    right--;
                } else if (nums[left] + nums[right] + a < 0) {
                    left++;
                } else {
                    v.push_back({nums[i], nums[left], nums[right]});
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++; // 持续移动left直到找到不同元素
                    }
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--; // 持续移动right直到找到不同元素
                    }
                    left++;
                    right--;
                }
            }
            // 对i进行去重
            while (i + 1 < nums.size() && nums[i + 1] == nums[i]) {
                i++;
            }
        }
        return v;
    }
};

leetcode18例题

算法原理讲解:

解法一:排序+暴力枚举+去重

解法二:沿用三数之和的思想:排序+双指针+去重

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        sort(nums.begin(), nums.end());
        vector<vector<int>> ret;
        // 增加边界判断,避免数组长度不足4的情况
        if (nums.size() < 4) {
            return ret;
        }
        for (int a = 0; a < nums.size(); a++) {
            for (int b = a + 1; b < nums.size(); b++) {
                // 在left和right区间
                long long sum = (long long)target - nums[a] - nums[b]; // 剩下两个数相加=sum
                int left = b + 1;
                int right = nums.size() - 1;
                while (left < right) {
                    if (nums[left] + nums[right] < sum) {
                        left++;
                    } else if (nums[left] + nums[right] > sum) {
                        right--;
                    } else {
                        // 相等
                        ret.push_back(
                            {nums[a], nums[b], nums[left], nums[right]});
                        // 去重left和right
                        while (left < right && nums[left + 1] == nums[left]) {
                            left++;
                        }
                        while (left < right && nums[right - 1] == nums[right]) {
                            right--;
                        }
                        left++;
                        right--;
                    }
                }
                // 去重b
                while (b < nums.size()-2 && nums[b + 1] == nums[b]) {
                    b++;
                }
            }
            // 去重a
            while (a < nums.size()-3 && nums[a + 1] == nums[a]) {
                a++;
            }
        }
        return ret;
    }
};

双指针的理解

当题目符合 “需要遍历数组 / 链表,且暴力解法是 O (n²) 或更高,同时元素有排序 / 单调性 / 特定位置关系” 时,就可以优先考虑双指针。

遇到题目时,按这 3 步走,就能快速决策:

  1. 第一步:先想暴力解法,看时间复杂度比如 “三数之和”,暴力是三重循环(O (n³)),显然效率低;这时候就想 “能不能用更少的循环?”—— 双指针能把三重循环降为 O (n²)(排序 O (nlogn),遍历 i + 左右指针 O (n²)),符合优化需求。

  2. 第二步:看题目是否有 “单调性 / 排序 / 位置差” 的条件

    • 如果数组是无序的,但暴力解法复杂度高,试试 “先排序” 再用双指针(如三数之和、四数之和);
    • 如果是链表,且要找中点 / 环 / 倒数 k 个元素,直接用快慢指针(不用额外空间,比用栈 / 哈希表更优);
    • 如果是子数组问题,且元素非负(保证窗口扩大时和递增,缩小时有递减),用滑动窗口(双指针维护窗口)。
  3. 第三步:排除其他更优解法比如 “两数之和”(无序数组),用哈希表是 O (n),比双指针(先排序 O (nlogn)+ 左右指针 O (n))更优,这时候就不用双指针;但如果是 “两数之和 II”(有序数组),双指针比哈希表更省空间(O (1) vs O (n)),这时候优先用双指针。

如果题目符合以下情况,大概率不用双指针:

  • 暴力解法已经是 O (n) 或 O (logn)(比如 “找数组中的最大值”,直接遍历 O (n),不用双指针);
  • 数组无序且排序后会破坏题目条件(比如 “找数组中两数之和的索引”,排序会丢索引,这时候用哈希表);
  • 子数组问题中元素有正有负(滑动窗口的单调性被打破,比如 “和为 0 的子数组”,用前缀和 + 哈希表更优)。

对于滑动窗口的理解请看一下节

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值