基础算法--滑动窗口

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;
}
相关推荐
ゞ 正在缓冲99%…5 分钟前
leecode300.最长递增子序列
算法·动态规划
浮生如梦_35 分钟前
C#Halcon深度学习预热与否的运行时间测试
图像处理·人工智能·深度学习·算法·计算机视觉·c#·视觉检测
TANGLONG2221 小时前
【C++】穿越时光隧道,拾贝史海遗珍,轻启C++入门之钥,解锁程序之奥秘(首卷)
java·c语言·数据结构·c++·redis·python·算法
星迹日1 小时前
数据结构:时间复杂度和空间复杂度
数据结构·算法·时间空间复杂度
走在考研路上1 小时前
力扣编程从0-1
python·算法·leetcode
武昌库里写JAVA1 小时前
Springboot 升级带来的Swagger异常
数据结构·vue.js·spring boot·算法·课程设计
dundunmm2 小时前
【论文阅读】SCGC : Self-supervised contrastive graph clustering
论文阅读·人工智能·算法·数据挖掘·聚类·深度聚类·图聚类
码农老起2 小时前
深入解析希尔排序:原理、实现与优化
数据结构·算法·排序算法
庞传奇3 小时前
【LC】191. 位1的个数
java·数据结构·算法·leetcode
Lenyiin4 小时前
02.01、移除重复节点
c++·算法·leetcode