经典算法——相向双指针

基本逻辑

双指针,顾名思义就是使用两个指针来遍历元素,而要想使用两个指针,必须有额外的条件,最典型的就是有序数组。

下面来举一个最最最最典型的例子说明。

167. 两数之和 II - 输入有序数组https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/description/

两数之和等于target,可以说是双指针题目的鼻祖了。

一般一下子能想到的就是暴力枚举了,但是时间复杂度在O(n^2),显然太低效了。

暴力枚举,只用到了 numbers 是一个数组,但仔细看题目给出了 numbers 按非递减顺序排列,也就是有序数组。所以我们需要把这个条件也用上,去优化时间复杂度。

下面给出暴力枚举和双指针,对比讲解。简单来说,双指针就是正向遍历和逆向遍历同时进行。

int n = numbers.size();

/*
l 代表的是正向遍历,r 代表的从逆向遍历

因为numbers数组是非递减的
那么就有numbers[l] <= numbers[l+i] (i = 1, 2, 3...)
同样numbers[r] >= numbers[r-i] (i = 1, 2, 3...)
*/
int l = 0, r = n-1;

//直接遍历
for (l; l < n; l++) {
    for (r; r >l ; r--) {
        if (numbers[l] == numbers[r]) {
            return {l, r};
        }
    }
}

//使用双指针
while (l < r) {
    /*
      当numbers[l] + numbers[r] < target时,
      则numbers[l] + numbers[r-i] < numbers[l] + numbers[r] < target
      所以不需要再用r依次遍历 l+1 ~ r-1,因为它们肯定都是小于target
    */  
    if (numbers[l] + numbers[r] < target) {
        l++;
    }
    //同理,不需要用l依次遍历 l+1 ~ r-1,因为它们肯定都是大于target
    else if (numbers[l] + numbers[r] > target) {
        r--;
    }
    else {
        return {l, r};
    }
}

基本的模版就是这样了,大家可以去LeetCode写完上面那题。

另外,双指针还有一种用法,用来判断是否为回文串,也是双指针的经典题目。

125. 验证回文串https://leetcode.cn/problems/valid-palindrome/description/?envType=problem-list-v2&envId=x5grcpId

看起来和前面的两数之和不一样,但本质上都是双指针,只是改了判断条件,所以大家在用双指针的时候一定要灵活,不要太死板。

string s;

int l = 0, r = s.size();

while (l < r) {
    if (s[l] == s[r]) {
        l++; r--;
    }
    else {
        return false;
    }
}

return true;

上面就是判断回文串的核心代码,大家可以去写一下。

双指针的应用

实践巩固一下,举几个题目,灵活使用双指针。

15. 三数之和https://leetcode.cn/problems/3sum/description/

这道题就是前面两数之和模版题的一个升级(实际上就是枚举 target),需要注意的是,nums数组无序,所以我们要先排序才能使用双指针。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        //排序,以便后面使用双指针。
        sort(nums.begin(), nums.end());
        vector<vector<int>> ans;
        int size = nums.size();
        for (int i = 0; i < size-2; i++) {
            if (i>0 && nums[i] == nums[i-1]) continue;

            //枚举target
            int target = 0 - nums[i];
            
            //优化:如果最小的两个数大于target,那么后面的数都会大于target,直接break
            if (nums[i+1] + nums[i+2] > target) break;

            //优化:如果最大的两个数小于target,那么前面的数都会小于target,继续遍历循环
            if (nums[size-1] + nums[size-2] < target) continue;

            int l = i + 1, r = size - 1;

            //双指针
            while(l < r) {
                //剔除重复
                if (l > i+1 && nums[l-1] == nums[l] && r < size - 1 && nums[r] == nums[r+1]) {
                    l++;r--;
                    continue;
                }

                if (nums[r] + nums[l] > target) {
                    r--;
                }
                else if (nums[r] + nums[l] < target) {
                    l++;
                }
                else {
                    ans.push_back({nums[i], nums[l], nums[r]});
                    l++;r--;
                }
            }
        }
        return ans;
    }
};

18. 四数之和https://leetcode.cn/problems/4sum/description/?envType=problem-list-v2&envId=x5grcpId

这道题是三数之和的升级,实际上就是再多加一个枚举,大家可以去尝试一下,我这边直接给出代码。

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        ranges::sort(nums);
        int size=nums.size();
        vector<vector<int>> ans;           
        set<vector<int>> exist;
        //枚举第一个数
        for(int i=0;i<size-3;i++){
            //枚举第二个数
            for (int j=i+1;j<size-2;j++){
                //剩下来的两个数就用双指针枚举
                long long pre_sum=nums[i]+nums[j];
                int l=j+1,r=size-1;
                
                if (pre_sum+nums[j+1]+nums[j+2]>target) break;
                if (pre_sum+nums[size-1]+nums[size-2]<target) continue;

                while(l<r){
                    long long cur=pre_sum+nums[l]+nums[r];
                    if (cur==target){
                        vector<int> temp={nums[i],nums[j],nums[l],nums[r]};
                        if (exist.count(temp)==0){
                            exist.insert(temp);
                            ans.push_back(temp);
                        }
                        l++;r--;
                    }
                    else if (cur>target) r--;
                    else l++;
                }
            }
        }
        return ans;
    }
};

相较于前面的有点难度,这题要注意数据范围,需要开long long

P8708 [蓝桥杯 2020 省 A1] 整数小拼接https://www.luogu.com.cn/problem/P8708

#include <bits/stdc++.h>
using ll = long long;
using namespace std;

//判断 string a 是否大于 string b
bool compare(string a, string b) {
    if (a.size() > b.size()) {
        return true;
    }
    else if (a.size() == b.size() && a > b) {
        return true;
    }
    
    return false;
}

int main() {
    int n;
    string k;
    cin >> n >> k;

    //long long 型 k
    ll k_num = stoll(k);

    vector<ll> nums;
    for (int i = 0; i < n; i++) {
        ll tmp;
        cin >> tmp;
        nums.push_back(tmp);
    }

    int size = nums.size();
    //排序,为双指针做准备
    sort(nums.begin(), nums.end());

    int l = 0, r = size-1;
    ll ans = 0;

    //双指针
    while (l < r) {
        //拼接
        string cur = to_string(nums[l]) + to_string(nums[r]);
        if (compare(cur, k)) {
            r--;
        }
        else {
            //如果 l 和 r 拼接 <= k,那么,l不变,r的范围在[l+1, r]拼接 <= k,共计 r-l 符合要求
            ans += r-l;
            l++;
        }
    }

    //判断逻辑上面那个双指针是一样的,只是拼接的方式不同
    l = 0, r = size-1;
    while (l < r) {
        string cur = to_string(nums[r]) + to_string(nums[l]);
        if (compare(cur, k)) {
            r--;
        }
        else {
            ans += r-l;
            l++;
        }
    }

    cout << ans << endl;
}

一些心得

算法这种东西容易忘,需要的是平时的积累,如果上面的题目写不出来,没关系,可以先看一遍博主的题解(但一定要自己先思考过了,比如用暴力枚举做出来了,那也是一种进步),抄一下通关获点成就感,然后理解一下代码,等过几天再回来写。

ps:(其实就和打游戏一样,不断去闯关,去叠场次,像永劫里练连招、球弓这些都是依靠时间练出来的,捏蓝克烈他们都是玩了上万个小时;火影里主播的技术他们也是通过上万场才练出来的像我知道的心安,银色都是这样的,所以说,如果想让自己的算法实力变厉害,必须要投入时间,也许几道看不出来自己的提升,也许比别人慢,都没关系的,没有天赋,那就重复,大家可以去LeetCode、洛谷上刷题,写对自己来说简单的题目,相当于炸鱼,每天可以来几道,爽一爽,而对自己来说难的题目,才能更好提升实力,其实博主我也没什么实力,大家一起共勉吧),

大家想要参加算法比赛的一定要去练习,不参加的偶尔也要去写写,算法作为计算机基础,无论是找工作面试,还是考研都是必要的,而且能锻炼你的计算机思维。

感兴趣的可以给博主点个免费的关注,博主后面还会继续更新算法和其它知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值