基础算法之双指针与滑动窗口

//本文章为本人自己对该部分内容学习的理解,如有错误欢迎指正:>

双指针和滑动窗口都是算法的入门基础,两者有着异曲同工之处。

双指针的引入和介绍:       

 在对数组的特定子数组进行查找的题目中往往需要遍历整个数组,常常还不止多遍,面对数组数量过大的情况时间复杂度会大到惊人,利用两个指针不断调整区间从而求出最优解的方法就叫双指针,也称尺取法。

给个例题:2563. 统计公平数对的数目

给你一个下标从 0 开始、长度为 n 的整数数组 nums ,和两个整数 lower 和 upper ,返回 公平数对的数目 。

如果 (i, j) 数对满足以下情况,则认为它是一个 公平数对 :

  • 0 <= i < j < n,且
  • lower <= nums[i] + nums[j] <= upper

示例 1:

输入:nums = [0,1,7,4,4,5], lower = 3, upper = 6
输出:6
解释:共计 6 个公平数对:(0,3)、(0,4)、(0,5)、(1,3)、(1,4) 和 (1,5) 。

示例 2:

输入:nums = [1,7,9,2,5], lower = 11, upper = 11
输出:1
解释:只有单个公平数对:(2,3) 。

面对这样的题目,双指针就能大展身手了。由于只需返回个数而不是位置,可以先进行排序,通过定下指针l和r的位置,我们只需要考虑这个区间内小于lower和小于upper的个数了,而公平数对就是这两个个数的差。

代码如下:

int cmp(const void *a, const void *b){
    int va = *(int*)a;
    int vb = *(int*)b;
    return va < vb ? -1 : 1;
}

long long countlessThan(int* nums, int n, int val){
    int l = 0, r = n - 1;
    long long ans = 0;
    while(l < r){
        int sum = nums[l] + nums[r];
        if(sum <= val){
            ans += (r - l);
            ++l;
        }else{
            r--;
        }
    }
    return ans;
}

long long countFairPairs(int* nums, int n, int lower, int upper){
    int ans = 0, len = 0;
    qsort(nums, n, sizeof(int), cmp);
    long long a = countlessThan(nums, n, upper);
    long long b = countlessThan(nums, n, lower - 1);
    return a - b;
}

滑动窗口的引入和介绍:

         在平时的解题中会遇见如求最长无重复元素的子数组这类问题,首先想到的就是遍历数组。给一串笨笨的代码。

        例题:3. 无重复字符的最长子串

int findmaxlen(char* s, int sSize){
    int len = 0;
    for(int i = 0; i < sSize; i++){
        for(int j = i+1; j < numsSize; j++){
            if(s[i]!=s[j]&&len < j - i + 1){
                len = j - i + 1;
            }else break;
        }
    }
}

不难发现这串代码的时间复杂度太大了如果面对numsSize过大的情况下很有可能超时。

不如换种想法:将一个左指针l放在0,一个右指针r放在-1,这个时候数组的长度为空,用哈希表hash[256]将子数组内各元素出现次数置零,再向右移动右指针。移动过程中,如果hash[s[r]]为0的话就自增;如果大于1的话就说明,在l~r这个区间内有了两个重复的元素,先将hash[s[l]]自减,再自增l直到hash[s[r]]小于等于1为止。之后再判断最长的长度。

该想法就是根据不断调整指针减少重复操作次数。

转换成代码:

int lengthOfLongestSubstring(char* s) {
    int ans = 0, i = 0, j = -1, n = strlen(s);
    int h[256];
    memset(h, 0, sizeof(h));
    while(j < n - 1){
        ++j;
        ++h[s[j]];
        while(h[s[j]] > 1){
            --h[s[i]];
            ++i;
        }
        if(j - i + 1 > ans)
            ans = j - i + 1;
    }
    return ans;
}

现在给个新题目1493. 删掉一个元素以后全为 1 的最长子数组

给你一个二进制数组 nums ,你需要从中删掉一个元素。

请你在删掉元素的结果数组中,返回最长的且只包含 1 的非空子数组的长度。

如果不存在这样的子数组,请返回 0 。

提示 1:

输入:nums = [1,1,0,1]
输出:3
解释:删掉位置 2 的数后,[1,1,1] 包含 3 个 1 。

示例 2:

输入:nums = [0,1,1,1,0,1,1,0,1]
输出:5
解释:删掉位置 4 的数字后,[0,1,1,1,1,1,0,1] 的最长全 1 子数组为 [1,1,1,1,1] 。

示例 3:

输入:nums = [1,1,1]
输出:2
解释:你必须要删除一个元素。

题目要求在一串连续的元素中只能有一个0且1的数量最多,根据滑动窗口的套路,将l,r初始化后再慢慢通过这个条件来更新窗口。

代码如下:

int longestSubarray(int* nums, int numsSize) {
    int l = 0, r = -1, len = 1,zerocnt = 0;
    while(r < numsSize - 1){
        r++;
        zerocnt += !nums[r];
        while(zerocnt > 1){
            zerocnt -= !nums[l];
            l++;
        }
        if(len < r - l + 1){
            len = r - l + 1;
        }
    }
    return len - 1;
}

在双指针和滑动窗口的套路下,代码是不是显得简洁明了呢?还同时大大降低了时间复杂度。

两者定义与特点:

        1.滑动窗口依靠左右指针的移动来标记窗口的边界,指针的移动就是窗口的滑动,而窗口的大小就可以根据指针移动来调整。滑动窗口常用于解决连续子串或连续数组类型的问题,即窗口内的元素是连续的,例如求无重复字符最长字符串,最大子数组等等。

        2.双指针则是指在遍历数组或字符串中,使用两个及以上的指针来进行遍历,这两个指针常指向不同的元素,根据需要进行调整。双指针多用来降低算法的时间复杂度,在一次循环内进行多次操作,避免多重循环。关键在于指针的起始位置以及移动速度和方向。可用于解决数组中的两数之和、三数之和、回文子串等问题。

区别:

1.指针指向和移动:

        ·滑动窗口的左右指针移动方向可能相同可能相反用于改变窗口大小(如左指针向左移动,右指针向右移动,扩大窗口大小),具有窗口的概念。

        ·双指针的指向和移动较为自由随机,可能指向数组中的任意位置,根据问题要求进行调整。

2.窗口概念:

        ·滑动窗口中窗口根据指针调整,大小是动态变化的。

        ·双指针中没有明确的“窗口”概念,因为指针的位置是任意的,不一定构成连续的窗口。

3.应用场景:

        ·滑动窗口更适合解决连续子串或子数组的问题。

        ·双指针倾向于数组中元素的查找,排序,匹配问题。

4.复杂度:

        二者都能有效的降低算法的时间复杂度,主要根据自身的移动策略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值