基础算法--滑动窗口

1.长度最小的子数组

解法一:暴力解法

时间复杂度:O(N^2)

算法思路:

从前往后枚举数组中的任意⼀个元素,把它当成起始位置。然后从这个「起始位置」开始,然 后寻找⼀段最短的区间,使得这段区间的和「⼤于等于」⽬标值。 将所有元素作为起始位置所得的结果中,找到最⼩值即可。

cpp 复制代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        // 记录结果

            
        int ret = INT_MAX;
        int n = nums.size();
        // 枚举出所有满⾜和⼤于等于target的⼦数组 [start, end]
        // 由于是取到最⼩,因此枚举的过程中要尽量让数组的⻓度最⼩
        //  枚举开始位置

            for (int start = 0; start < n; start++)
            {
                int sum = 0; //  记录从这个位置开始的连续数组的和
                // 寻找结束位置

                    for (int end = start; end < n; end++)
                    {
                        sum += nums[end]; // 将当前位置加上


                            if (sum >= target) // 当这段区间内的和满⾜条件时

                            {
                                // 更新结果,start开头的最短区间已经找到
                                ret = min(ret, end - start + 1);
                                break;
                            }
                    }
            }
        // 返回最后结果

            return ret == INT_MAX ? 0 : ret;
    }
};

解法二:滑动窗口

时间复杂度:O(N)

算法思路:

由于此问题分析的对象是「一段连续的区间」,因此可以考虑「滑动窗口」的思想来解决这道题。

让滑动窗口满足:从 i 位置开始,窗口内所有元素的和小于 target(那么当窗口内元素之和第一次大于等于目标值的时候,就是 i 位置开始,满足条件的最小长度)。

做法:将右端元素划入窗口中,统计出此时窗口内元素的和:

  • 如果窗口内元素之和大于等于 target:更新结果,并且将左端元素划出去的同时继续判断是否满足条件并更新结果(因为左端元素可能很小,划出去之后依旧满足条件)
  • 如果窗口内元素之和不满足条件:right++,另下一个元素进入窗口。

为何滑动窗⼝可以解决问题,并且时间复杂度更低?

  • 这个窗口寻找的是:以当前窗口最左侧元素(记为 left1)为基准,符合条件的情况。也就是在这道题中,从 left1 开始,满足区间和 sum >= target 时的最右侧(记为 right1)能到哪里。

  • 我们既然已经找到从 left1 开始的最优的区间,那么就可以大胆舍去 left1。但是如果继续像方法一一样,重新开始统计第二个元素(left2)往后的和,势必会有大量重复的计算(因为我们在求第一段区间的时候,已经算出很多元素的和了,这些和是可以在计算下次区间和的时候用上的)。

  • 此时,right1 的作用就体现出来了,我们只需将 left1 这个值从 sum 中剔除。从 right1 这个元素开始,往后找满足 left2 元素的区间(此时 right1 也有可能是满足的,因为 left1 可能很小。sum 剔除掉 left1 之后,依旧满足大于等于 target)。这样我们就能省掉大量重复的计算。

  • 这样我们不仅能解决问题,而且效率也会大大提升。

时间复杂度:虽然代码是两层循环,但是我们的 left 指针和 right 指针都是不回退的,两者最多都往后移动 n 次。因此时间复杂度是 O (N)。

cpp 复制代码
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int left = 0,right = 0;
        int n = nums.size();
        int sum = 0;
        int len = INT_MAX;

        for(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.无重复字符的最长字串

算法一:暴力解法

算法思路: 枚举从每⼀个位置开始往后,⽆重复字符的⼦串可以到达什么位置。找出其中⻓度最⼤的即可。 在往后寻找⽆重复⼦串能到达的位置时,可以利⽤哈希表统计出字符出现的频次,来判断什么 时候⼦串出现了重复元素。

cpp 复制代码
//暴力解法
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int ret = 0;  // 记录结果

        int n = s.length();
        // 1. 枚举从不同位置开始的最长重复子串
        // 枚举起始位置
        for (int i = 0; i < n; i++) {
            // 创建一个哈希表,统计频次
            int hash[128] = { 0 };
            // 寻找结束为止
            for (int j = i; j < n; j++) {
                hash[s[j]]++;  // 统计字符出现的频次
                if (hash[s[j]] > 1) {  // 如果出现重复的
                    break;
                }
                // 如果没有重复,就更新ret
                ret = max(ret, j - i + 1);
            }
        }
        // 2. 返回结果
        return ret;
    }
};

算法二:

算法思路: 研究的对象依旧是⼀段连续的区间,因此继续使⽤「滑动窗⼝」思想来优化。 让滑动窗⼝满⾜:窗⼝内所有元素都是不重复的。

做法:右端元素 left进⼊窗⼝的时候,哈希表统计这个字符的频次: 如果这个字符出现的频次超过 1 ,说明窗⼝内有重复元素,那么就从左侧开始划出窗⼝, 直到 left这个元素的频次变为 1 ,然后再更新结果。 如果没有超过 1 ,说明当前窗⼝没有重复元素,可以直接更新结果。

cpp 复制代码
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int hash[128] = {0};
        int left = 0,right = 0,n = s.size();
        int ret = 0;
        while(right<n)
        {
            hash[s[right]]++;//进窗口
            while(hash[s[right]]>1)//判断
            {
                hash[s[left++]]--;//出窗口
            }
            //更新
            ret = max(ret,right-left+1);
            right++;
        }
         return ret;
    }

   
};

3.最大连续1的个数III

类似这种连续区间,我们可以考虑使用滑动窗口

算法思路:滑动窗口

初始化一个计数器;初始化一些变量 left = 0,right = 0,ret = 0;

当right小于数组大小的时候,一直有以下循环

让当前元素进入窗口,同时要统计0的个数:如果0的数量超过了k,那么我们需要让左侧元素滑出窗口,顺便更新我们的计数器,直到我们的计数器恢复为小于k。

然后right继续向后走,处理下一个元素。

循环结束后,ret 存的就是最终结果。

cpp 复制代码
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        int left = 0,right = 0,n = nums.size();
        int count = 0;//计数
        int ret = 0;
        while(right<n)
        {
            if(nums[right]==0)
            {
                count++;
            }
            while(count>k)//判断0的数量是否合法
            {
                if(nums[left++]==0)//出窗口
                {
                    count--;
                }
                
            }
            ret = max(ret,right-left+1);
            right++;//入窗口
        }

        return ret;
    }
};

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

算法思路:滑动窗口,时间复杂度O(N)

转化问题:

将问题转化为求 target = sum(nums) - x。

如果 target < 0,问题无解,return -1。

初始化:

左右指针 left = 0,right= 0(滑动窗口区间表示为 [l, r],左右区间是否开闭很重要,必须设定与代码一致)。

记录当前滑动窗口内数组和的变量 sum = 0。

记录当前满足条件数组的最大区间长度 maxLen = -1。

循环条件:

进窗口

当 r 小于等于数组长度时,一直循环:

如果 sum < target,右移右指针,直至变量和大于等于 target,或右指针已经移到头。

出窗口

如果 sum > target,右移左指针,直至变量和小于等于 target,或左指针已经移到头。

等于target时才会更新

如果经过前两步的左右移动使得 sum == target,维护满足条件数组的最大长度,并让下个元素进入窗口。

循环结束后:

如果 maxLen 的值有意义,则计算结果返回;否则,返回 -1。

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

5.水果成蓝

这道题目,我们先来思考一下暴力解法,暴力解法的思路比较简答,给一个hash表统计每一个水果出现了多少种,就是把所有符合要求的长度都枚举出来,取最长的即可。

然后讲解一下为什么要用滑动窗口来解决:

滑动窗口解题思路:

a.初始化哈希表hash来统计窗⼝内⽔果的种类和数量;

b. 初始化变量:左右指针left=0,right=0,记录结果的变量ret=0;

c. 当right⼩于数组⼤⼩的时候,⼀直执⾏下列循环:

i. 将当前⽔果放⼊哈希表中;

ii. 判断当前⽔果进来后,哈希表的⼤⼩:

如果超过2:

将左侧元素滑出窗⼝,并且在哈希表中将该元素的频次减⼀;

如果这个元素的频次减⼀之后变成了0,就把该元素从哈希表中删除;

重复上述两个过程,直到哈希表中的⼤⼩不超过2;

iii. 更新结果ret;

iv. right++,让下⼀个元素进⼊窗⼝;

d. 循环结束后,ret存的就是最终结果。

这个长度是有限制的,所以我们这里直接用一个自定义一个hash数组就行了,可以提高一点效率。

cpp 复制代码
class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        // 用于记录每种水果出现的频次,数组大小设为一个较大值,因为水果种类的值范围已经给定,这里设为100001
        int hash[100001] = {0};  
        int ret = 0;
        int n = fruits.size();
        // 滑动窗口的左右指针,kind用于记录当前窗口内水果的种类数
        for (int left = 0, right = 0, kind = 0; right < n; right++) {
            // 当新水果进入窗口时
            if (hash[fruits[right]] == 0) {
                kind++;
            }
            hash[fruits[right]]++;

            // 如果窗口内水果种类超过2种
            while (kind > 2) {
                // 将左指针指向的水果移出窗口
                hash[fruits[left]]--;
                if (hash[fruits[left]] == 0) {
                    kind--;
                }
                left++;
            }
            // 更新最长符合条件的子数组长度
            ret = max(ret, right - left + 1);
        }
        return ret;
    }
};

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

算法思路:滑动窗口(定长滑窗)

时间复杂度O(N)

因为字符串 p 的异位词的⻓度⼀定与字符串 p 的⻓度相同,所以我们可以在字符串 s 中构 造⼀个⻓度为与字符串 p 的⻓度相同的滑动窗⼝,并在滑动中维护窗⼝中每种字⺟的数量;

当窗⼝中每种字⺟的数量与字符串 p 中每种字⺟的数量相同时,则说明当前窗⼝为字符串 p 的异位词;

因此可以⽤两个⼤⼩为 26 的数组来模拟哈希表,⼀个来保存 s 中的⼦串每个字符出现的个 数,另⼀个来保存 p 中每⼀个字符出现的个数。这样就能判断两个串是否是异位词。

cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int len = p.size();
        int hash1[26] = { 0 };
        for (auto ch : p)
        {
            hash1[ch - 'a']++;
        }
        int len1 = len;
        vector<int> vt;
        int left = 0, right = 0;

        int hash2[26] = { 0 };
        while (right < s.size())
        {
            hash2[s[right] - 'a']++;//进窗口
            if (right - left + 1 > len)//出窗口
            {
                hash2[s[left] - 'a']--;
                left++;
            }
            if (right - left + 1 == len)//判断
            {
                int i = 0;
                while (hash1[i++] == hash2[i])
                {
                    if (i == 25)
                    {
                        vt.push_back(left);
                        break;
                    }
                }
                
            }
            
            right++;
        }
        return vt;
    }
};

小优化:

我们这里给一个count 来统计窗口当中有效字符个数。

cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int len = p.size();
        int hash1[26] = { 0 };//统计字符串p中出现的字符个数
        for (auto ch : p)
        {
            hash1[ch - 'a']++;
        }
        int count = 0;//
        vector<int> vt;
        int left = 0, right = 0;
        int hash2[26] = { 0 };//统计窗口中每一个字符出现的个数
        while (right < s.size())
        {
            hash2[s[right] - 'a']++;//进窗口
            if(hash2[s[right]-'a']<=hash1[s[right]-'a'])
            {
                count++;
            }
            if (right - left + 1 > len)
            {
                if(hash2[s[left]-'a']<=hash1[s[left]-'a'])
                {
                    count--;
                }
                hash2[s[left] - 'a']--;//出窗口
                left++;
            }

            if (count==len)
            {
                
                vt.push_back(left);
                   
            }
            
            right++;
        }
        return vt;
    }
};

7.串联所有子串

算法思路:滑动窗口(同上题)

时间复杂度 O(N)

如果我们把每⼀个单词看成⼀个⼀个字⺟,问题就变成了找到「字符串中所有的字⺟异位词」。⽆ ⾮就是之前处理的对象是⼀个⼀个的字符,我们这⾥处理的对象是⼀个⼀个的单词。

代码:

cpp 复制代码
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        unordered_map<string,int> hash1;//保存word里面所有单词的频次
        for(auto& s:words)
        {
            hash1[s]++;
        }
        vector<int> vt;
        int len = words[0].size(),m = words.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); 
                hash2[in]++;
                if(hash1.count(in)&&hash2[in]<=hash1[in])
                {
                    count++;
                }
                //判断
                if(right-left+1>len*m)
                {
                    //出窗口,维护count
                    string out = s.substr(left,len);
                    if(hash1.count(out)&&hash2[out]<=hash1[out])
                    {
                        count--;
                    }   
                    hash2[out]--;
                    left+=len;
                }
                //更新结果
                if(count==m)
                {
                    vt.push_back(left);
                }
            }
        }
        return vt;
    }
};

8.最小覆盖字串

做题思路:

最开始想的是和之前一样用哈希表统计两个字符串中的字母数量,然后用滑动窗口在该区间内对比。这题我们依然可以发现left左移,right不用右移,所以自然可以想到用滑动窗口解题。

滑动窗口的套路都差不多,但是这里如果用count优化时,需要处理好count的数量,不像之前,我们的hash[out]<=hash[out],count--即可,因为这里我们出窗口的时候,窗口内的有效字母是可以重复的,需要特别注意一下。

附加代码:

cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_map>


class Solution {
public:
    std::string minWindow(std::string s, std::string t) {
        std::unordered_map<char, int> hash1;
        // 存储目标字符串 t 中字符及其出现的次数
        for (auto& c : t) {
            hash1[c]++;
        }
        std::string str;
        int len = t.size();
        int minLen = INT_MAX, ret = 0;
        std::unordered_map<char, int> hash2;
        for (int left = 0, right = 0, count = 0; right < s.size(); right++) {
            // 进窗口
            char in = s[right];
            hash2[in]++;
            // 如果当前字符是目标字符串中的字符,并且其出现次数不超过目标字符串中的次数,增加计数
            if (hash1[in]&&hash2[in] <= hash1[in]) {
                count++;
            }
            // 判断是否满足包含 t 中的所有字符
            while (count == len) {
                ret = right - left + 1;
                if (ret < minLen) {
                    minLen = ret;
                    str = s.substr(left, ret);
                }
                // 出窗口
                char out = s[left];
                hash2[out]--;
                // 如果出窗口的字符是目标字符串中的字符,并且其出现次数小于目标字符串中的次数,减少计数
                if (hash1[out]&&hash2[out] < hash1[out]) {
                    count--;
                }
                left++;
            }
        }
        return str;
    }
};

优化思路:

这里发现用count统计字母有效数量对比起来似乎有点麻烦,其实可以发现,我们只需要统计有效字母的种类。比如hash1中A的数量有3个,那么只有当hash2中A的数量等于3时,我们的count才会++,为什么不大于的时候也加呢,大于不也是有效的吗,大于的时候我们的count会多加的,比如在下图中这个例子中,大于等于的话,B会让count加2次,但这时count的数量是符合去判断的,但窗口里只有ABB,是不符合覆盖子串的,记住,我们统计的是种类,不是数量,那么出窗口的时候也是同理,在出窗口前,如果hash2[out]==hash1[out]时count才会--,我们这里的判断条件是count == hash.size();

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        int hash1[128] = { 0 };
        int hash2[128] = { 0 };
        int kinds = 0;//计算hash1的大小
        for (auto ch : t)
        {
            if (hash1[ch]++ == 0)
                kinds++;
        }
        int minlen = INT_MAX, begin = -1;
        for (int left = 0, right = 0, count = 0; right < s.size(); right++)
        {   //count用来统计有效字符的种类,不是数量。
            //进窗口
            char in = s[right];
            if (++hash2[in] == hash1[in])//维护count
            {
                count++;
            }
            while (count == kinds)//判断
            {
                if (right - left + 1 < minlen)//更新
                {
                    minlen = right - left + 1;
                    begin = left;
                }
                char out = s[left];
                if (hash2[out]-- == hash1[out])//出窗口
                {
                    count--;
                }
                left++;
            }
        }
        return begin == -1 ? "" : s.substr(begin, minlen);
    }
};
int main() {
    Solution sol;
    std::string s = "ABBCA";
    std::string t = "ABC";
    std::string result = sol.minWindow(s, t);
    std::cout << result << std::endl;
    return 0;
}
相关推荐
DASXSDW9 分钟前
NET性能优化-使用RecyclableBuffer取代RecyclableMemoryStream
java·算法·性能优化
kfepiza15 分钟前
CAS (Compare and Swap) 笔记251007
java·算法
墨染点香44 分钟前
LeetCode 刷题【103. 二叉树的锯齿形层序遍历、104. 二叉树的最大深度、105. 从前序与中序遍历序列构造二叉树】
算法·leetcode·职场和发展
啊我不会诶1 小时前
23ICPC澳门站补题
算法·深度优先·图论
Brookty2 小时前
【算法】二分查找(一)朴素二分
java·学习·算法·leetcode·二分查找
黑色的山岗在沉睡3 小时前
LeetCode 2761. 和等于目标值的质数对
算法·leetcode·职场和发展
bawangtianzun3 小时前
重链剖分 学习记录
数据结构·c++·学习·算法
T1an-17 小时前
力扣70.爬楼梯
算法·leetcode·职场和发展
T1an-17 小时前
力扣169.多数元素
数据结构·算法·leetcode
_dindong12 小时前
动规:回文串问题
笔记·学习·算法·leetcode·动态规划·力扣