算法二:滑动窗口

上篇文章详细讲解了双指针各种场景的运用,相信大家对双指针都有了一定的了解,本篇文章我们再来了解另一种特殊场景的双指针,该场景为左右指针共同维护一段连续的区间,通过特定情况改变左右边界使该区间不断变化,最终得到我们想要的结果,我们把这个方法称为------滑动窗口。

同样的,我们将通过八道题来对该算法进行场景详解。

目录

1.长度最小的的子数组

题解:

代码:

2.无重复字符的最长字串

题解:

代码:

[3.最大连续1的个数 III](#3.最大连续1的个数 III)

题解:

代码:

4.将x减到0的最小操作数

题解:

代码:

5.水果成篮

题解:

代码:

6.找到字符串中所有字母异位词

题解:

代码:

7.串联所有单词的字串

题解:

代码:

8.最小覆盖字串

题解:

代码:


1.长度最小的的子数组

209. 长度最小的子数组 - 力扣(LeetCode)

题解:

先将左右两指针left和right放在数组下标为0的位置,right指针不断向右移动,并把对应的数累加到sum变量里(进窗口),当sum的值大于等于target时,更新最短长度len(len最开始初始化为INT_MAX),再把left对应的值从sum减掉,同时left++,改变区间(出窗口),left继续向右移动,sum继续累加,直到再次使sum大于等于target,如果此时的区间长度小于len,则更新len,再重复以上操作,直到right从数组中越界,结束操作,此时返回len即可,但需要再注意的是,如果len仍为INT_MAX,证明不存在符合条件的子数组,此时要返回0,因此可以通过三目操作符返回。

代码:

cpp 复制代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int n = nums.size(), sum = 0, len = INT_MAX;
        for(int left = 0, right = 0; right < n; right++)
        {
            sum += nums[right];
            while(sum >= target)
            {
                len = min(len,right-left+1);
                sum-=nums[left];
                left++;
            }
        }
        return len == INT_MAX ? 0 : len;
    }
};

2.无重复字符的最长字串

3. 无重复字符的最长子串 - 力扣(LeetCode)

题解:

和上一题的道理其实差不多,依旧是定义两个指针放在下标为0的位置,right指针向右移动,上一题是通过sum来记录累加,这道题要找出无重复字符的最长字串,那么可以把right对应的字符放在哈希表里,同时记录出现的次数(进窗口),这很容易想到用unordered_map来进行记录,当出现重复字符,也就是right对应的字符出现的次数大于1时,将left对应的字符在哈希表里出现的次数减去1,同时left++(出窗口),直到right字符出现的次数变为1。若没有出现重复字符且此时的区间长度大于len,则更新len的长度(len初始化为0),这样可以保证记录出现重复字符前的无重复最长字串的长度,重复以上操作,最后right越界则结束操作,返回len即可。

代码:

cpp 复制代码
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int left = 0, right = 0, len = 0;
        unordered_map<char,int> mp;
        while(right < s.size())
        {
            mp[s[right]]++;
            while(mp[s[right]] > 1)
            {
                mp[s[left++]]--;
            }
            len = max(len, right-left+1);
            right++;
        }
        return len;
    }
};

当然,除了使用哈希容器之外,还可以使用长度128的静态数组,代码运行更高效。(参考ASCII码表),代码如下:

cpp 复制代码
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int left = 0, right = 0, len = 0;
        int mp[128] = {0};
        while(right < s.size())
        {
            mp[s[right]]++;
            while(mp[s[right]] > 1)
            {
                mp[s[left++]]--;
            }
            len = max(len, right-left+1);
            right++;
        }
        return len;
    }
};

3.最大连续1的个数 III

1004. 最大连续1的个数 III - 力扣(LeetCode)

题解:

这道题的题目其实有一个很巧妙的转化方式,它说可以把k个0翻转为1,并返回数组中连续1的最大个数,那是不是可以转化为返回不超过k个0的最大区间?那这道题用滑动窗口就显而易见了。

定义双指针在下标0的位置,right不断往右走(进窗口),同时记录0出现的个数,如果0的次数大于k了,那么left就要向右移动(出窗口),改变区间,同时出窗口一个0就把0的次数减1 ,直到0的次数再次等于k,则结束出窗口,如果0的次数小于等于k,则记录目前的长度,如果大于len(初始化为0),则更新len的值,重复以上操作,right越界则操作结束,最后返回len即可。

代码:

cpp 复制代码
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int left = 0, right = 0, len = 0;
        int n = nums.size();
        int zero = 0;
        while(right < n)
        {
            if(nums[right] == 0) zero++;
            while(zero > k)
            {
                if(nums[left] == 0) zero--;
                left++;
            }
            len = max(len,right-left+1);
            right++;
        }
        return len;
    }
};

4.将x减到0的最小操作数

1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)

题解:

这道题同样也有一个非常巧妙的转化方法,原题目要求只能减去最左边或者最右边元素的值,最后使x变为0,相当于要最左边的部分元素与最右边的部分元素相加为x,且要用最少的元素,那我先算出一整个数组所有元素累加sum的值,然后令target = sum - x,那题目是不是可以转化为找到最常的子数组,使子数组的元素相加为target,同时记录子数组的长度,最后用数组的长度减去最长子数组的长度,就得到了最少操作数。

需要单独考虑的是,如果target值小于0,那就意味着即使x把数组所有元素都减掉也不可能为0,直接返回-1。依旧定义双指针,right向右移动,同时把元素的值累加到tmp中,如果tmp大于target了,此时开始出窗口,left向右移动,同时将出窗口的元素从tmp中减去,直到tmp小于等于target,如果tmp == target且此时的区间长度大于len(len初始化为-1),更新len,重复以上操作,right越界则操作结束,此时看len是否更新过,如果len依旧为-1,证明没有符合题意的情况,直接返回即可,如果不为-1,则用数组长度减去len,即可得到最小操作数,返回即可。

代码:

cpp 复制代码
class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        int sum = 0;
        for(auto e : nums) sum += e;
        int target = sum - x;
        int len = -1;
        
        if(target < 0) return -1;
       
        for(int left = 0, right = 0, tmp = 0; right < nums.size(); right++)
        {
            tmp += nums[right];
            while(tmp > target)
            {
                tmp -= nums[left];
                left++;
            }
            if(tmp == target)
            {
                len = max(len,right-left+1);
            }
        }
        return len == -1 ? len : nums.size() - len;
    }
};

5.水果成篮

904. 水果成篮 - 力扣(LeetCode)

题解:

可以借助哈希表进行记录,记录每一个种类的水果以及其数目,同样利用双指针,right向右移动的同时记录入哈希表,用哈希表的size来记录水果的种类,如果记录的种类超过2,则进行出窗口操作,left向右移动,并把出窗口的对应的水果数目减1,当哈希表内某种水果的数目变为0的时候,将其从哈希表中删除,此时哈希表内的种类变为2,停止出窗口操作。如果哈希表内的水果种类小于等于2,且区间长度大于len,则更新len为最大区间长度,重复以上操作,right越界则停止操作,最后返回len即可。

代码:

cpp 复制代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        unordered_map<int,int> hash;
        int len = 0;
        for(int left = 0, right = 0; right < fruits.size(); right++)
        {
            hash[fruits[right]]++;
            while(hash.size() > 2)
            {
                hash[fruits[left]]--;
                if(hash[fruits[left]] == 0)
                {
                    hash.erase(fruits[left]);
                }
                left++;
            }
            len = max(len,right-left+1);
        }
        return len;
    }
};

6.找到字符串中所有字母异位词

438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

题解:

先定义一个静态哈希表hash1,遍历字符串p,将其中所有字母以及出现的数目记录到哈希表中,然后定义变量count(关键)和第二个静态哈希表hash2。定义两个指针在下标为0的位置,开始遍历字符串s,将right对应的字符记录到hash2中,记录其出现的次数,如果记录到hash2的该字符的次数小于等于hash1内该字符的次数,则让count++。当left和right之间的区间的长度大于字符串p的长度时,开始出窗口操作,如果left对应的字符在hash2的数目小于或等于在hash1的数目,则count--,然后left++,同时该字符在hash2内记录的次数也减1。当count == p.size()的时候,此时区间内的字串就是符合题意的异位词,而left就是起始索引,把left记录到vector即可。重复以上操作,right越界则操作结束,最后返回该vector即可。

代码:

cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> ret;
        int hash1[26] = {0};
        for(auto e: p)
        {
            hash1[e-'a']++;
        }
        int count = 0;
        int hash2[26] = {0};
        for(int left = 0, right = 0; right < s.size(); right++)
        {
            char in = s[right];
            hash2[in-'a']++;
            if(hash2[in-'a'] <= hash1[in-'a'])
            {
                count++;
            }
            while(right-left+1 > p.size())
            {
                char out = s[left];
                if(hash2[out-'a'] <= hash1[out-'a'])
                {
                    count--;
                }
                hash2[out-'a']--;
                left++;
            }
            if(count == p.size())
            {
                ret.push_back(left);
            }
        }
        return ret;
    }
};

7.串联所有单词的字串

30. 串联所有单词的子串 - 力扣(LeetCode)

题解:

本题与上一题有异曲同工之处,上一题我们记录的是字符,本道题则要记录字符串。先定义一个哈希表hash1,遍历字符串数组words,将里面的字符串以及出现的个数记录到hash1中,设字符串数组words的长度为m,以及数组中每一个字符串的长度为len(每一个字符串长度相同)。这次定义的双指针的right往右也不再是一步一步走了,而是一个字符串一个字符串地走,因此,left和right的起始位置要考虑len种,所以最外层要套一层for循环(i=0;i<len),里面定义第二个哈希表hash2,left和right的初始位置下标为i,同时定义count为0(关键),right向右移动,从right开始,把长度为len的字符串放入hash2中,如果该字符串在hash2的数目小于等于hash1内的数目,则count++,如果区间长度大于了words内所有字符串加起来的长度(len*m),则进行出窗口操作,从left开始,长度为len的字符串,如果它在hash2的次数小于等于在hash1的次数,则count--,然后该字符串在hash2的次数也减1,left向右移动len长度。如果count == m,则此时的区间内的字符串是符合题目要求的字串,left为起始索引,将left加入到vector中,循环重复以上操作即可,最后返回该vector即可。

代码:

cpp 复制代码
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> ret;
        unordered_map<string,int> hash1;
        for(auto e:words)
        {
            hash1[e]++;
        }
        int m = words.size();
        int len = words[0].size();
        for(int i = 0; i < len; i++)
        {
            unordered_map<string,int> hash2;
            for(int left = i, right = i, count = 0; right + len <= s.size(); right += len)
            {
                string in = s.substr(right,len);
                if(++hash2[in] <= hash1[in])
                {
                    count++;
                }
                while(right-left+1 > len * m)
                {
                    string out = s.substr(left,len);
                    if(hash2[out] <= hash1[out])
                    {
                        count--;
                    }
                    hash2[out]--;
                    left += len;
                }
                if(count == m)
                {
                    ret.push_back(left);
                }
            }
        }
        return ret;
    }
};

8.最小覆盖字串

76. 最小覆盖子串 - 力扣(LeetCode)

题解:

先定义一个静态哈希表hash1,同时遍历字符串t,将其中每个字符和对应的出现次数记录到hash1中,设m为t的长度,再定义第二个静态哈希表hash2和count(关键),定义双指针在下标为0的位置,right向右移动,同时把对应的字符以及出现次数记录到hash2中,如果该字符在hash2中出现的次数小于等于在hash1出现的次数,则count++,就这样一直往右走一直加,直到count == m,此时区间内就覆盖了t中所有的字符,如果区间长度小于minlen(初始化定义为INT_MAX),则更新minlen为该区间长度,同时更新begin的位置为当前left的位置,即最小覆盖字串的初始位置(begin的初始值为-1),同时进行出窗口操作,如果left对应的字符在hash2出现的次数小于等于在hash1出现的次数,则count--,循环结束,right继续移动,否则count不变,无论count是否变化,该字符在hash2的次数减1,同时left++,如果count不变,则循环继续,那么最小区间又可以继续更新。如此重复即可,最终判断begin是否仍为-1即未更新,若是,则不存在符合题意的字串,返回空串,若更新,则minlen必定也更新,此时返回从begin位置开始长度为minlen的字串即可。

代码:

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        int hash1[256] = {0};
        for(char ch:t)
        {
            hash1[ch]++;
        }
        int m = t.size();
        int minlen = INT_MAX, begin = -1//begin更新每次遇到最小合法区间的初始位置;
        int hash2[256] = {0};
        for(int left = 0, right = 0, count = 0; right < s.size(); right++)
        {
            //进窗口+维护count
            char in = s[right];
            if(++hash2[in] <= hash1[in])
            {
                count++;
            }
            while(count == m)
            {
                int len = right - left + 1;
                if(len < minlen)
                {
                    minlen = len;
                    //更新初始位置
                    begin = left;
                }
                //出窗口+维护count
                char out = s[left];
                if(hash2[out] <= hash1[out])
                {
                    count--;
                }
                hash2[out]--;
                left++;
            }
        }
        return begin == -1 ? "" : s.substr(begin, minlen);
    }
};

本次滑动窗口算法讲解到此结束,感谢观看,后续还会更新更多的算法知识,敬请期待。

相关推荐
仰泳的熊猫19 小时前
1081 Rational Sum
数据结构·c++·算法·pat考试
java修仙传19 小时前
力扣hot100题解:合并区间
算法·leetcode·职场和发展
橘颂TA19 小时前
【剑斩OFFER】算法的暴力美学——库存管理 III
算法·力扣
Hcoco_me19 小时前
大模型面试题16:SVM 算法详解及实践
算法·数据挖掘·聚类
wearegogog1231 天前
光谱分析波段选择的连续投影算法
算法
执笔论英雄1 天前
【RL】DAPO 数据处理
算法
why1511 天前
面经整理——算法
java·数据结构·算法
悦悦子a啊1 天前
将学生管理系统改造为C/S模式 - 开发过程报告
java·开发语言·算法
痕忆丶1 天前
双线性插值缩放算法详解
算法