LeetCode 滑動窗口(Sliding Window)類問題總結

導語java

滑動窗口類問題是面試當中的高頻題,問題自己其實並不複雜,可是實現起來細節思考很是的多,想着想着可能由於變量變化,指針移動等等問題,致使程序反覆刪來改去,有思路,可是程序寫不出是這類問題最大的障礙。本文會將 LeetCode 裏面的大部分滑動窗口問題分析、總結、分類,並提供一個能夠參考的模版,相信能夠有效減小面試當中的算法實現部分的不肯定性。面試


題目概覽

滑動窗口這類問題通常須要用到雙指針來進行求解,另一類比較特殊則是須要用到特定的數據結構,像是 sorted_map。後者有特定的題型,後面會列出來,可是,對於前者,題形變化很是的大,通常都是基於字符串和數組的,因此咱們重點總結這種基於雙指針的滑動窗口問題。算法

題目問法大體有這幾種:數組

  • 給兩個字符串,一長一短,問其中短的是否在長的中知足必定的條件存在,例如:
    • 求長的的最短子串,該子串必須涵蓋短的的全部字符
    • 短的的 anagram 在長的中出現的全部位置
    • ...
  • 給一個字符串或者數組,問這個字符串的子串或者子數組是否知足必定的條件,例如:
    • 含有少於 k 個不一樣字符的最長子串
    • 全部字符都只出現一次的最長子串
    • ...

除此以外,還有一些其餘的問法,可是不變的是,這類題目脫離不開主串(主數組)和子串(子數組)的關係,要求的時間複雜度每每是 O(n),空間複雜度每每是常數級的。之因此是滑動窗口,是由於,遍歷的時候,兩個指針一前一後夾着的子串(子數組)相似一個窗口,這個窗口大小和範圍會隨着先後指針的移動發生變化。bash


解題思路

根據前面的描述,滑動窗口就是這類題目的重點,換句話說,窗口的移動就是重點。咱們要控制先後指針的移動來控制窗口,這樣的移動是有條件的,也就是要想清楚在什麼狀況下移動,在什麼狀況下保持不變,我在這裏講講個人想法,個人思路是保證右指針每次往前移動一格,每次移動都會有新的一個元素進入窗口,這時條件可能就會發生改變,而後根據當前條件來決定左指針是否移動,以及移動多少格。我寫來一個模版在這裏,能夠參考:數據結構

public int slidingWindowTemplate(String[] a, ...) {
    // 輸入參數有效性判斷
    if (...) {
        ...
    }
    
    // 申請一個散列,用於記錄窗口中具體元素的個數狀況
    // 這裏用數組的形式呈現,也能夠考慮其餘數據結構
    int[] hash = new int[...];
    
    // 預處理(可省), 通常狀況是改變 hash
    ...
    
    // l 表示左指針
    // count 記錄當前的條件,具體根據題目要求來定義
    // result 用來存放結果
    int l = 0, count = ..., result = ...;
    for (int r = 0; r < A.length; ++r) {
        // 更新新元素在散列中的數量
        hash[A[r]]--;
        
        // 根據窗口的變動結果來改變條件值
        if (hash[A[r]] == ...) {
            count++;
        }
        
        // 若是當前條件不知足,移動左指針直至條件知足爲止
        while (count > K || ...) {
            ...
            if (...) {
                count--;
            }
            hash[A[l]]++;
            l++;
        }
        
        // 更新結果
        results = ...
    }
    
    return results;
}
複製代碼

這裏面的 「移動左指針直至條件知足」 部分,須要具體題目具體分析,其餘部分的變化不大,我如今來看看具體的題目。框架


具體題目分析及代碼

438. Find All Anagrams in a Stringpost

別看這是一道 easy 難度的題目,若是限定你在 O(n) 時間複雜度內實現呢?按照模版會很簡單,首先窗口是固定的,窗口長度就是輸入參數中第二個字符串的長度,也就是說,右指針移動到某個位置後,左指針必須跟着一同移動,且每次移動都是一格,模版中 count 用來記錄窗口內知足條件的元素,直到 count 和窗口長度相等便可更新答案spa

public List<Integer> findAnagrams(String s, String p) {
    if (s.length() < p.length()) {
        return new ArrayList<Integer>();
    }
    
    char[] sArr = s.toCharArray();
    char[] pArr = p.toCharArray();
    
    int[] hash = new int[26];
    
    for (int i = 0; i < pArr.length; ++i) {
        hash[pArr[i] - 'a']++;
    }
    
    List<Integer> results = new ArrayList<>();
    
    int l = 0, count = 0, pLength = p.length();
    for (int r = 0; r < sArr.length; ++r) {
        hash[sArr[r] - 'a']--;
        
        if (hash[sArr[r] - 'a'] >= 0) {
            count++;
        }
        
        if (r > pLength - 1) {
            hash[sArr[l] - 'a']++;
            
            if (hash[sArr[l] - 'a'] > 0) {
                count--;
            }
            
            l++;
        }
        
        if (count == pLength) {
            results.add(l);
        }
    }
    
    return results;
}
複製代碼

76. Minimum Window Substring指針

一樣是兩個字符串之間的關係問題,由於題目求的最小子串,也就是窗口的最小長度,說明這裏的窗口大小是可變的,這裏移動左指針的條件變成,只要左指針指向不須要的字符,就進行移動

public String minWindow(String s, String t) {
    if (s.length() < t.length()) {
        return "";
    }
    
    char[] sArr = s.toCharArray();
    char[] tArr = t.toCharArray();
        
    int[] hash = new int[256];
    
    for (int i = 0; i < tArr.length; ++i) {
        hash[tArr[i]]++;
    }
    
    int l = 0, count = tArr.length, max = s.length() + 1;
    String result = "";
    for (int r = 0; r < sArr.length; ++r) {
        hash[sArr[r]]--;
        
        if (hash[sArr[r]] >= 0) {
            count--;
        }
        
        while (l < r && hash[sArr[l]] < 0) {
            hash[sArr[l]]++;
            l++;
        }
        
        if (count == 0 && max > r - l + 1) {
            max = r - l + 1;
            result = s.substring(l, r + 1);
        }
    }
    
    return result;
}
複製代碼

159. Longest Substring with At Most Two Distinct Characters

這題窗口大小仍是變化的,重點仍是在什麼時候更新左指針。這裏的 count 能夠用來記錄當前有多少種不一樣的字符,只要當前進入窗口的字符使 count 不知足題目條件,咱們就移動左指針讓 count 知足條件。這裏 result 初始化爲 1,由於只要輸入的字串不是空的,窗口大小不可能小於 1。

public int lengthOfLongestSubstringTwoDistinct(String s) {
    if (s == null || s.length() == 0) {
        return 0;
    }
    
    char[] sArr = s.toCharArray();
    
    int[] hash = new int[256];
    
    int l = 0, count = 0, result = 1;
    for (int r = 0; r < sArr.length; ++r) {
        hash[sArr[r]]++;
        
        if (hash[sArr[r]] == 1) {
            count++;
        }
        
        while (count > 2) {
            hash[sArr[l]]--;

            if (hash[sArr[l]] == 0) {
                count--;
            }

            l++;
        }
        
        result = Math.max(result, r - l + 1);
    }
    
    return result;
}
複製代碼

340. Longest Substring with At Most K Distinct Characters

和上題同樣,把 2 變成 k 便可

public int lengthOfLongestSubstringKDistinct(String s, int k) {
    if (s == null || s.length() == 0 || k == 0) {
        return 0;
    }
    
    char[] sArr = s.toCharArray();
    
    int[] hash = new int[256];
    
    int l = 0, count = 0, result = 1;
    for (int r = 0; r < sArr.length; ++r) {
        hash[sArr[r]]++;
        
        if (hash[sArr[r]] == 1) {
            count++;
        }
        
        while (count > k) {
            hash[sArr[l]]--;

            if (hash[sArr[l]] == 0) {
                count--;
            }

            l++;
        }
        
        result = Math.max(result, r - l + 1);
    }
    
    return result;
}
複製代碼

3. Longest Substring Without Repeating Characters

輸入只有一個字符串,要求子串裏面不可以有重複的元素,這裏 count 都不須要定義,直接判斷哈希散列裏面的元素是否是在窗口內便可,是的話得移動左指針去重。

public int lengthOfLongestSubstring(String s) {
    if (s == null || s.length() == 0) {
        return 0;
    }
    
    char[] sArr = s.toCharArray();
    int[] hash = new int[256];
    
    int l = 0, result = 1;
    for (int r = 0; r < sArr.length; ++r) {
        hash[sArr[r]]++;
        
        while (hash[sArr[r]] != 1) {
            hash[sArr[l]]--;
            l++;
        }
        
        result = Math.max(result, r - l + 1);
    }
    
    return result;
}
複製代碼

567. Permutation in String

和 438 那題很相似,可是這裏不須要記錄答案了,有就直接返回 true。

public boolean checkInclusion(String s1, String s2) {
    if (s1.length() > s2.length()) {
        return false;
    }
    
    char[] s1Arr = s1.toCharArray();
    char[] s2Arr = s2.toCharArray();
    
    int[] hash = new int[26];
    
    for (int i = 0; i < s1Arr.length; ++i) {
        hash[s1Arr[i] - 'a']++;
    }
    
    int l = 0, count = 0;
    for (int r = 0; r < s2Arr.length; ++r) {
        hash[s2Arr[r] - 'a']--;
        
        if (hash[s2Arr[r] - 'a'] >= 0) {
            count++;
        }
        
        if (r >= s1Arr.length) {
            hash[s2Arr[l] - 'a']++;
            
            if (hash[s2Arr[l] - 'a'] >= 1) {
                count--;
            }
            
            l++;
        }
        
        if (count == s1Arr.length) {
            return true;
        }
    }
    
    return false;
}
複製代碼

992. Subarrays with K Different Integers

看完了字符串類型的題目,此次來看看數組類型的,題目中的 subarray 已經明確了這個題能夠考慮用滑動窗口,這題比較 trick 的一個地方在於,這裏不是求最小值最大值,而是要你計數,可是若是每次僅僅加 1 的話又不太對,例如 A = [1,2,1,2,3], K = 2 這個例子,假如右指針移到 index 爲 3 的位置,若是按以前的思路左指針根據 count 來移動,當前窗口是 [1,2,1,2],可是怎麼把 [2,1] 給考慮進去呢?能夠從數組和子數組的關係來思考,假如 [1,2,1,2] 是符合條件的數組,若是要計數的話,[1,2,1,2] 要求的結果是否和 [1,2,1] 的結果存在聯繫?這兩個數組的區別在於多了一個新進來的元素,以前子數組計數沒考慮到這個元素,假如把這個元素放到以前符合條件的子數組中組成的新數組也是符合條件的,咱們看看這個例子中全部知足條件的窗口以及對應的知足條件的子數組狀況:

[1,2,1,2,3]  // 窗口知足條件
 l r         // 知足條件的子數組 [1,2]
 
[1,2,1,2,3]  // 窗口知足條件
 l   r       // 知足條件的子數組 [1,2],[2,1],[1,2,1]
 
[1,2,1,2,3]  // 窗口知足條件
 l     r     // 知足條件的子數組 [1,2],[2,1],[1,2,1],[1,2],[2,1,2],[1,2,1,2]

[1,2,1,2,3]  // 窗口不知足條件,移動左指針至知足條件
 l       r   

[1,2,1,2,3]  // 窗口知足條件
       l r   // 知足條件的子數組 [1,2],[2,1],[1,2,1],[1,2],[2,1,2],[1,2,1,2],[2,3]
複製代碼

你能夠看到對於一段連續的數組,新的元素進來,窗口增長 1,每次的增量都會在前一次增量的基礎上加 1。當新的元素進來打破當前條件會使這個增量重新回到 1,這樣咱們左指針移動條件就是隻要是移動不會改變條件,就移動,否則就中止

public int subarraysWithKDistinct(int[] A, int K) {
    if (A == null || A.length < K) {
        return 0;
    }
    
    int[] hash = new int[A.length + 1];
    
    int l = 0, results = 0, count = 0, result = 1;
    for (int r = 0; r < A.length; ++r) {
        hash[A[r]]++;
        
        if (hash[A[r]] == 1) {
            count++;
        }
        
        while (hash[A[l]] > 1 || count > K) {
            if (count > K) {
                result = 1;
                count--;
            } else {
                result++;
            }
            hash[A[l]]--;
            l++;
        }
        
        if (count == K) {
            results += result;
        }
    }
    
    return results;
}
複製代碼

424. Longest Repeating Character Replacement

這道題想 accept 的話不難,可是問題在於怎麼知道當前窗口中數量最多的字符的數量,由於須要替換的字符就是當前窗口的大小減去窗口中數量最多的字符的數量。最簡單的方法就是把哈希散列遍歷一邊找到最大的字符數量,可是仔細想一想若是咱們每次新進元素都更新這個最大數量,且只更新一次,咱們保存的是當前遍歷過的全局的最大值,它確定是比實際的最大值大的,咱們左指針移動的條件是 r - l + 1 - maxCount > k,保存的結果是 result = Math.max(r - l + 1, result); 這裏 maxCount 比實際偏大的話,雖然致使左指針不能移動,可是不會記錄當前的結果,因此最後的答案並不會受影響

public int characterReplacement(String s, int k) {
    if (s == null || s.length() == 0) {
        return 0;
    }
    
    char[] sArr = s.toCharArray();
    
    int[] hash = new int[26];
    
    int l = 0, maxCount = 0, result = 0;
    for (int r = 0; r < sArr.length; ++r) {
        hash[sArr[r] - 'A']++;
        
        maxCount = Math.max(maxCount, hash[sArr[r] - 'A']);
        
        while (r - l + 1 - maxCount > k) {
            hash[sArr[l] - 'A']--;
            l++;
        }
        
        result = Math.max(r - l + 1, result);
    }
    
    return result;
}
複製代碼

其餘題型

239. Sliding Window Maximum

能夠參考我以前寫的 Arts 的算法部分 -> Arts 第三週


480. Sliding Window Median

求中值的問題比較特殊,若是是亂序的狀況下最好就是藉助堆,利用兩個堆,一個大頂,一個小頂,而且保證兩個堆內存放的數目相差不超過 1,這樣若是當前元素數目是偶數,兩個堆頂元素的均值就是結果,若是是奇數,存放元素多的那個堆的堆頂元素就是結果。

public double[] medianSlidingWindow(int[] nums, int k) {
    if (nums == null || nums.length < k ) {
        return new double[0];
    }
    
    double[] results = new double[nums.length - k + 1];
    
    PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
    
    for (int i = 0; i < nums.length; ++i) {
        // add current element into queue
        maxHeap.offer(nums[i]);
        minHeap.offer(maxHeap.poll());
        
        if (minHeap.size() > maxHeap.size()) {
            maxHeap.offer(minHeap.poll());
        }
        
        // record answer
        if (i >= k - 1) {
            results[i - k + 1] = minHeap.size() < maxHeap.size() 
                    ? maxHeap.peek() : ((long)maxHeap.peek() + minHeap.peek()) * 0.5;

            if (maxHeap.contains(nums[i - k + 1])) {
                maxHeap.remove(nums[i - k + 1]);
            } else {
                minHeap.remove(nums[i - k + 1]);
            }
        }
    }
    
    return results;
}
複製代碼

總結

雙指針類的滑動窗口問題思惟複雜度並不高,可是出錯點每每在細節。記憶經常使用的解題模版仍是頗有必要的,特別是對於這種變量名多,容易混淆的題型。有了這個框架,思考的點就轉化爲 「什麼條件下移動左指針」,無關信息少了,思考加實現天然不是問題。

相關文章
相關標籤/搜索