聊聊演算法——滑動窗口

有看到一句話,我深以為然:「所有演算法的終極數據結構只有兩種:數組和鏈表!」其他所有數據結構都是數組或鏈表的衍生品,

不管是樹還是圖或者棧,至於演算法就最終都落到了這兩種結構的操作上,滑動窗口也不例外!滑動窗口的應用場景還是很多的:

HTTP的幀傳輸,滑動窗口限流演算法、Flink中的滑動窗口等,今天,我們就來聊聊滑動窗口的演算法框架!

作者原創文章,謝絕一切轉載,違者必究!

準備:

Idea2019.03/JDK11.0.4

難度: 新手–戰士–老兵–大師

目標:

  1. 滑動窗口演算法分析與應用

1 演算法框架

先給出滑動窗口演算法框架:

/* 滑動窗口演算法框架 */
private static void minWindow(String s,String t){
    // 滑動窗口左右側位置指針
    int left = 0,right = 0;
    while (right < s.length()){
        // 滑動窗口右邊增加
        right ++;
        if ( 滑動窗口滿足目標){
            //對窗口內元素操作 
        }
        // 滑動窗口左邊縮小
        while ( 滑動窗口縮小條件 ){
            // 
            left ++;
        }
    }
}

我們以實際例子來加強理解,來個 hard 級別的玩一下,力扣第76題:

給你一個字元串 S、一個字元串 T,請在字元串 S 裡面找出:包含 T 所有字元的最小子串:

示例,輸入: S = “ADOBECODEBANC”, T = “ABC”,輸出: “BANC”,如果不存在則返回空串。

先直接寫出解法如下(Java),略長,耐心看完必有收穫:

public class SlideWindow {
    public static void main(String[] args) {
        String s = "DBABECFCAB";
        String t = "ABC";
        System.out.println(minWindow(s,t));
    }
    private static String minWindow(String s,String t){
        if (s.length()==0 || t.length() ==0){
            return "NULL";
        }
        // 目標字元串使用Map存儲,如果有重複的,則數量累加
        Map<Character,Integer> dictT = new HashMap<>(8);
        for (int i = 0; i < t.length(); i++) {
            int count = dictT.getOrDefault(t.charAt(i),0);
            dictT.put(t.charAt(i),count +1);
        }
        // 目標字元串的長度
        int required = dictT.size();
        // 滑動窗口左右側位置指針
        int left = 0,right = 0;
        // 滑動窗口中字元串統計
        Map<Character,Integer> winStrMap = new HashMap<>(16);
        // 窗口內子串與目標串字元匹配的個數
        int formed = 0;
        // 記錄最小窗口長度,{窗口長,left,right}
        int[] ans = {-1,0,0};
        //窗口進行滑動
        while (right < s.length()){
            char c = s.charAt(right);
            int count = winStrMap.getOrDefault(c,0);
            winStrMap.put(c,count + 1);
            // 匹配一個字元則記錄一次
            if (dictT.containsKey(c) && winStrMap.get(c).intValue() == dictT.get(c).intValue()){
                formed ++;
            }
            while (left < right && formed == required){
                c = s.charAt(left);
                //計算最小窗口長
                if( ans[0] == -1 || (right - left + 1 < ans[0])){
                    ans[0] = right - left + 1;
                    ans[1] = left;
                    ans[2] = right;
                }
                winStrMap.put(c, winStrMap.get(c) -1);
                if (dictT.containsKey(c) && winStrMap.get(c) < dictT.get(c)){
                    formed --;
                }
                // 進行左側縮小滑動
                left ++;
            }
            //窗口向右滑動
            right ++;
        }
        return ans[0] == -1 ? "NULL" : s.substring(ans[1],ans[2]+1);
    }
}

以上程式碼解釋:使用兩個Map分別記錄目標字元串和滑動窗口內字元串的字元數量,並進行對比;在窗口向右滑動的過程中,

如果滿足目標條件則停下來進行窗口左側縮小滑動,並記錄下可行解的結果;窗口再向右滑動,並繼續尋找可行解,直到右

側到達終點。 此題有改良思路,即先對搜索字元串中去掉不存在於目標字元串中的字元,這樣滑動匹配次數就更少了。

我做了一個動圖,解釋找到第一個可行解 {ABEC},既首次匹配到{A:1,B:1,C:1}的過程,注意此解不是最終解:

2 應用秒殺

既然明白了演算法框架思路,來做秒殺,題1,力扣第 567 題Medium級別:

給定兩個字元串 s1 和 s2,寫一個函數來判斷 s2 是否包含 s1 的排列。示例,輸入: s1 = “ab”,

s2 = “eidbaooo”,輸出: True,解釋: s2 包含 s1 的排列之一 (“ba”).

參考上面的演算法,直接裁剪一下就出來了(Java):

private static boolean subString(String s,String t){
    if (s.length()==0 || t.length() ==0){
        return false;
    }
    Map<Character,Integer> targetMap = new HashMap<>(8);
    for (int i = 0; i < t.length(); i++) {
        // 目標串中可能有重複字元,
        int count = targetMap.getOrDefault(t.charAt(i),0);
        targetMap.put(t.charAt(i),count + 1);
    }
    Map<Character,Integer> winMap = new HashMap<>(16);
    int left = 0,right = 0 ;
    int match = 0;
    while(right < s.length()){
        char c = s.charAt(right);
        int count = winMap.getOrDefault(c,0);
        winMap.put(c,count + 1);
        // 右側一直向右滑動,直到包含了目標串的所有字元排列
        if (targetMap.containsKey(c) && targetMap.get(c).intValue() == winMap.get(c)){
            match ++;
        }
        right ++;
        // 窗口左側向右滑動,判斷滿足所有字元排列的子串
        while (left < right && match == targetMap.size()){
            // 如果子串長度等於目標串長度,即為可行解
            if (right - left + 1 == targetMap.size()){
                return true;
            }
            c = s.charAt(left);
            if (targetMap.containsKey(c) && targetMap.get(c).intValue() == winMap.get(c)){
                count = winMap.getOrDefault(c,0);
                winMap.put(c,count -1);
                match --;
            }
            left ++;
        }
    }
    return false;
}

程式碼解析:在窗口擴大滑動過程中,先找到包含了目標串所有字元的子串,然後左側滑動縮小,如果同時滿足

窗口中子串長度和目標串長度一樣,因為既包含了所有字元且長度一樣的子串肯定為一個排列,即為可行解!

 

秒殺題2,力扣第3題 Medium級別:

給定一個字元串,請你輸出其中不含有重複字元的最長子串。示例: 輸入, “ABEBCDEE”, 輸出:”EBCD”,

我這裡並非原題,做了個變形,要求輸出子串,而不是給出個長度值,解法如下:

/**查找最長無重複子串*/
private static String subString(String s) {
    if (s.length()==0){
        return "";
    }
    int left = 0,right = 0;
    Map<Character,Integer> winMap = new HashMap<>(8);
    int[] position={0,0,0};
    while (right < s.length()){
        char c = s.charAt(right);
        int count = winMap.getOrDefault(c,0);
        winMap.put(c, count + 1);
        if (right - left > position[0]){
            position[0]= right - left;
            position[1]= left;
            position[2]= right;
        }
        while (winMap.get(c) > 1){
            if (s.charAt(right) == s.charAt(left)){
                count = winMap.get(c);
                winMap.put(c,count - 1);
            }
            left ++;
        }
        right ++;
    }
    return s.substring(position[1],position[2]);
}

程式碼解析:如何找重複字元?即記錄字元個數並累加,大於1的即存在重複。同樣是滑動窗口,但注意使用一個

數組記錄可行解,然後對比其他可行解,並只保留最優解。注意,最長無重複子串不唯一,如”ABEBCDEE”,

可行解為”EBCD”或者”BCDE”,故原題僅輸出長度值而只有唯一解。另外,這裡其實有個優化思路,每次窗口右

側發現有重複字元,窗口左側滑動時,可以不必逐字元滑動,可以直接定位到重複字元的下一位即可。

 

後記:總以為演算法平時用的少,工作也不是演算法崗,所以沒必要去研究,可是遭到社會的毒打後,才知道演算法是很重要的,共勉!

 

全文完!


我近期其他文章:

只寫原創,敬請關注