• 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

  • 相关阅读:
    SecureRandom
    《Head First 设计模式》[02] 观察者模式
    《MySQL必知必会》[07] 管理事务处理
    《MySQL必知必会》[06] 触发器
    《MySQL必知必会》[05] 存储过程和游标
    Centos7安装Nginx
    IDEA配置Tomcat
    Java小功能大杂烩
    Java处理中文乱码问题
    Java邮件发送
  • 原文地址:https://www.cnblogs.com/cpaulyz/p/12401581.html
Copyright © 2020-2023  润新知