Sliding Window Algorithm 滑动窗口算法

简介

在LeetCode写题目的时候评论区看到一个方法,一开始没看懂,后来查了一些资料整理了一下。原题见文中例3

什么是滑动窗口算法?
The Sliding Problem contains a sliding window which is a sub – list that runs over a Large Array which is an underlying collection of elements.

滑动窗口算法可以用以解决数组/字符串的子元素问题,它可以将嵌套的循环问题,转换为单循环问题,降低时间复杂度。

假设有数组[a b c d e f g h]
一个大小为3的滑动窗口在其上滑动,则有:
[a b c]
[b c d]
 [c d e]
   [d e f]
     [e f g]
       [f g h]

算法题例子

例1

给定一个整数数组,计算长度为 'k' 的连续子数组的最大总和。

输入:arr [] = {100,200,300,400}
  k = 2

输出:700

解释:300 + 400 = 700

思路1:暴力法

没啥好说的,直接遍历,但是时间复杂度很差

C++代码:

int maxSum(int *arr, int length, int k) {
    int max = INT32_MIN;
    for (int i = 0; i < length - k + 1; i++) {
        int tempSum = 0;
        for (int j = 0; j < k; j++) {
            tempSum += arr[i + j];
        }
        max = tempSum > max ? tempSum : max;
    }
    return max;
}

思路2:滑动窗口

C++代码如下:

int maxSum(int *arr,int length,int k){
    int max=0;
    for (int i = 0; i < k; ++i) {
        max+=arr[i];
    } // 初始化max
    for (int j = 0; j < length-k; ++j) {
        int temp = max-arr[j]+arr[j+k];
        max = temp>max?temp:max;
    }
    return max;
}

例2

LeetCode原题

给定一个字符串 S 和一个字符串 T,请在 S 中找出包含 T 所有字母的最小子串。(minimum-window-substring)

输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"

思路:左右指针滑动窗口

这个问题让我们无法按照示例 1 中的方法进行查找,因为它不是给定了窗口大小让你找对应的值,而是给定了对应的值,让你找最小的窗口。

我们仍然可以使用滑动窗口算法,只不过需要换一个思路。

既然是找最小的窗口,我们先定义一个最小的窗口,也就是长度为 0 的窗口。

我们比较一下当前窗口在的位置的字母,是否是 T 中的一个字母。

很明显, A 是 ABC 中的一个字母,也就是 T 所有字母的最小子串 可能包含当前位置的 S 的值。

如果包含,我们开始扩大窗口,直到扩大后的窗口能够包含 T 所有字母。

假设题目是 在 S 中找出包含 T 所有字母的第一个子串,我们就已经解决问题了,但是题目是找到最小的子串,就会存在一些问题。

  • 当前窗口内可能包含了一个更小的能满足题目的窗口
  • 窗口没有滑动到的位置有可能包含了一个更小的能满足题目的窗口

为了解决可能出现的问题,当我们找到第一个满足的窗口后,就从左开始缩小窗口。

  1. 如果缩小后的窗口仍满足包含 T 所有字母的要求,则当前窗口可能是最小能满足题目的窗口,储存下来之后,继续从左开始缩小窗口。
  2. 如果缩小后的窗口不能满足包含 T 所有字母的要求,则缩小窗口停止,从右边开始扩大窗口。

缩小窗口停止:

向右扩大停止:

不断重复上面的步骤,直到窗口滑动到最右边,且找不到合适的窗口为止。最小满足的窗口就是我们要找的 S 中包含 T 所有字母的最小子串。

C++代码如下:

string maxSubString(string s,string t){
    map<char,int> rightData;
    for (int i = 0; i < t.length(); ++i) {
        if(rightData.find(t[i])!=rightData.end()){
            rightData[t[i]]++;
        } else{
            rightData[t[i]] = 1;
        }
    }
    int leftPos = 0;
    int rightPos = 0;
    // 窗口的左右指针
    int count = t.length(); // t中不被子串包含的字符数
    int min = INT32_MAX; // 最小长度
    string res;

    while (rightPos < s.length()){
        if(rightData.find(s[rightPos])!=rightData.end()){
            if(rightData[s[rightPos]]>0)
                count--;
            rightData[s[rightPos]]--;
        }
        rightPos++;
        while (count==0) { // 找到子串,左边向右收缩
            if(rightPos-leftPos<min){
                min = rightPos -leftPos;
                res = s.substr(leftPos,rightPos-leftPos);
            }
            if(rightData.find(s[leftPos])!=rightData.end()){
                rightData[s[leftPos]]++;
                if(rightData[s[leftPos]]>0)
                    count++;
            }
            leftPos++;
        }
    }
    return res;
}

在LeetCode评论区看到一个化简后的写法,其实里面的j就是rightPos,i就是leftPos

    int lengthOfLongestSubstring(string s) {
        int  size,i=0,j,k,max=0;
        size = s.size();
        for(j = 0;j<size;j++){
            for(k = i;k<j;k++)
                if(s[k]==s[j]){
                    i = k+1;
                    break;
                }
            if(j-i+1 > max)
                max = j-i+1;
        }
        return max;
    }

例3

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。(longest-substring-without-repeating-characters)

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

通过例2,我们发现这种滑动窗口的问题可以用一左一右两个指针来解决。

和例 2 相似,我们不断的扩大/缩小窗口,把无重复字母的窗口大小保存下来,直到窗口滑动结束,就找到了不含有重复字符的 最长子串 的长度。

思路

leftPos 窗口左指针

rightPos 窗口右指针

只要保证窗口内的子串没有重复字符即可,用map来记录

其实这也是遍历一遍所有符合条件的子串的方法,时间复杂度为O(n)

C++代码如下:

int lengthOfLongestSubstring(string str){
    map<char,int> strMap;
    int leftPos = 0;
    int rightPos = 0;
    int max = INT32_MIN;
    string res;
    while (rightPos<str.length()){
        if(strMap.find(str[rightPos])==strMap.end()){
            strMap[str[rightPos]] = 1;
            rightPos++;
        } else{
            while (leftPos<rightPos){
                if(str[leftPos] == str[rightPos]){
                    strMap.erase(str[leftPos]);
                    leftPos++;
                    break;
                } else{
                    strMap.erase(str[leftPos]);
                    leftPos++;
                }
            }
        }
        if(rightPos-leftPos>max){
            max = rightPos-leftPos;
            res = str.substr(leftPos,max);
        }
    }
    cout<<res<<endl;
    return max;
}

例4

给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。(find-all-anagrams-in-a-string)

输入:
s: "cbaebabacd" p: "abc"

输出:
[0, 6]

解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。

与示例 1 类似,我们维护一个长度为 p 的窗口,然后不断往右滑动查找当前窗口是否为 p 的字母异位词。

方法总结:

具体来说:

  1. 双指针begin,end——记录滑动窗口的左右边界。
  2. 一个Hash表——记录的t中的所有字符(去重)以及每个字符的出现次数。原因:由于t中可能包含重复字符,那么不仅要依次判断窗口子序列是否包含t中某个字符,还要判断该字符出现的次数是否与在t中相同。既然字符本身和出现次数相关联,那么就可以用一对键值对来表示,所以可使用Hash表来保存t中的字符和出现频率。C++中,我们用unordered_map<char, int> map;
  3. 一个计数器count,记录t中包含的字符数(去重后),即需要判断是否存在于t的字符。
  4. 令begin = 0, end = 0;移动右边界,每当发现一个字符存在于t中,递减该字符在Hash表中出现频次,即<key,value>中value的值,递减至0时,说明该窗口子序列中至少包含了与t中相同个数的该字符,那么此时递减count计数器,表示该字符的判断已完成,需要判断的字符数-1.
  5. 以此类推,不断拓展右边界,直至count为0,表示窗口序列中已经至少包含了t中所有字符(包括重复的)。
  6. 分析此时的窗口子序列,t是该序列的子集,条件2已满足。如果两者长度相同,即满足条件3,那么它的左边界begin就是我们想要的结果之一了。但我们不会一直那么幸运,这时就需要收缩窗口的左边界,即end不动,begin向右移动遍历该子序列,直至找到t中包含的字符,此时再次计算end-begin的值,与t长度比较,判断是否是想要的结果。而找到上述字符后,字符频次加1,如加1后该字符频次仍小于0,说明该字符有冗余,而出现频次大于0,则count加1,这是告诉我们有一个字符需要重新被判断了,因为无论它是不是我们想要的,都不能再用了,需要继续向右拓展窗口从新找起。
  7. 当count != 0时,继续向右拓展窗口,直至count为0,然后判断条件3的同时,向右移动begin遍历子序列,直至count != 0,以此类推。

参考链接

https://www.jianshu.com/p/869f6d00d962

https://www.zhihu.com/topic/20746237/intro

猜你喜欢

转载自www.cnblogs.com/cpaulyz/p/12401581.html