76 最小覆蓋子串(滑動窗口)

1. 問題描述:

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

示例:

輸入: S = "ADOBECODEBANC", T = "ABC"
輸出: "BANC"
說明:

如果 S 中不存這樣的子串,則返回空字符串 ""。
如果 S 中存在這樣的子串,我們保證它是唯一的答案。

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/minimum-window-substring

2. 思路分析:

① 對於這種題目來說,假如使用模擬的方法肯定超時而且容易出錯,使用滑動窗口的方法來解決就再適合不過了,因爲是第一次接觸滑動窗口所以看了一下官方的題解,一開始的時候感覺思路倒是不難理解,但是代碼一開始的時候沒有理解清楚,所以自己使用idea的debug進行調試,並且結合了官方的題解進行代碼的理解,下面是我自己看了題解之後自己整理的思路

② 滑動窗口簡單來說可以分爲兩個步驟:

1)先是找到滿足題目要求的範圍

2)在確定的範圍內進行範圍的縮小,使得得到的結果進一步縮小

對於這道題目來說,首先是要確定在S字符串中包含着T字符串的子串,比如S中的子串ADOBEC就包含了T中的所有字母,第二個是找到所有滿足包含T的所有字符的子串,可以發現存在ADOBEC、BECODEBA、BANC這些子串,在子串中找到最短的那個就是我們要求解的答案,所以來說思路還是比較清楚的

3)所以我們首先要找出所有包含ABC的子串,這裏可以使用數據結構List來進行映射,List中的元素可以爲Pair類型(類似於Map類型),裏面存放字符串T中字母在S中出現的位置,使用Pair類型的話那麼可以很方便地知道T中的字母在S中出現的位置在哪裏,使用這樣的數據結構來映射的話可以很容易計算出字符串的從哪些位置開始是包含着所有字母的,下面是使用debug截的圖可以很清楚看到映射關係:

4)使用List方法找到滿足T中所有字母的肯定是當前子串中最短的,因爲我們是使用S中包含的T的字母的位置來計算的,此外我們還需要在一開始的時候就map記錄在字符串T中出現的字母的次數,這樣在尋找下一個滿足包含所有T字符的子串的時候纔好計算,比如第一個找到的子串肯定是ADOBEC,肯定是當前最短的,因爲我們找到時候是根據T中出現的字母找的,下一次嘗試找到包含所有的,所以從List中找到T中出現的下一個字母的B的位置開始尋找(這個在List中可以很方便找到)但是需要刪除的是上一個字母,刪除的時候需要將另外一個記錄出現的字母次數的map中映射的字母數量減1,這樣在下一次尋找包含所有的纔是正確的,下一次是BEC但是這個時候不滿足包含所有所以需要擴展右邊在循環中找到包含T中ABC字符的子串,可以發現是BECODEBA,所以這個時候需要使用兩個while循環,第一個while循環是用來擴展右邊界的找到符合所有ABC字母的子串,第二個是縮小子串的範圍,找到更短長度的

5)所以進入第二個while循環的時候肯定是找到了滿足條件的,這個時候縮小子串範圍,看一下刪除掉上一個滿足條件的位置的字母是否有影響,比如找到第一個子串ADOBEC,嘗試刪除掉A是否滿足包含T中所有字母,假如滿足條件那麼繼續刪除可以發現這裏不行(因爲缺少了字母A),在刪除左邊字母的時候有時候會更短,因爲像找到的子串假如爲AAOBEC,刪除掉第一個A沒有什麼影響,但是得到的是更短的子串,所以這樣在刪除的沒有影響的話繼續刪除,刪除之後有影響的話那麼需要擴展右邊界

6)可以使用一個三個元素的數組來更新最短子串的長度,子串的開始位置與結束位置,這樣在最後的話可以得到最短子串的位置

7)其實理解整個思路之後還是比較簡單的,關鍵是使用對應的數據結構,所以我自己對官方的題解領悟之後按照自己的思路重新寫了一遍

8)核心是List<Pair<Character, Integer>>可以找到包含所有T字母的子串,Map<Character, Integer>用來統計T中字母出現次數這樣在刪除字母的時候纔好計算並且比較好找掉包含T中全部字母的

9)首先是需要理解官方給出的代碼,捋清思路假如不能理解清楚,藉助於idea的debug進行調試,逐步執行,觀察結果這樣可能理解會快一點,理解之後可以學習一下其中的數據結構,比如像Pair,很像Map只是在使用的時候好像不能夠修改但是存值與取值都是一樣的,看懂別人的代碼也是一種能力,看懂之後需要自己寫一遍,需要自己將錯誤修改過來這樣對於自己理解代碼纔是比較有幫助的

3. 代碼如下:

我自己寫的:

import java.util.*;
public class Solution {
    public String minWindow(String s, String t) {
        /*首先是計算出給出的t字符串中各個字符出現的次數這樣在接下來循環刪除上一個滿足條件的字母的時候纔好計算*/
        Map<Character, Integer> charTimes = new HashMap<>();
        for (int i = 0; i < t.length(); ++i){
            /*計算每個字符出現的次數*/
            char c = t.charAt(i);
            //getOrDefault方法可以不用判斷之前是否鍵是否存在避免空指針的異常
            charTimes.put(c, charTimes.getOrDefault(c, 0) + 1);
        }
        /*接下來通過計算字符串中t中每一個字符出現的位置在哪裏, 這些位置都是包含着t字符串中的字符的*/
        List<Pair<Character, Integer>> charPos = new ArrayList<>();
        for (int i = 0; i < s.length(); ++i){
            //計算出位置
            char c = s.charAt(i);
            if (charTimes.containsKey(c)){
                //將每個在t字符串中出現的字符標記在list中這樣在計算是否包含所有字符串的時候才方便一點
                charPos.add(new Pair<>(c, i));
            }
        }
        /*往字符串進行往右擴展, cur變量用來存儲當前在t中出現的匹配的字母個數*/
        int l = 0, r = 0, totalChars = t.length(), cur = 0;
        /*用來記錄歷史上的最小子串*/
        int ans[] = {-1, 0, 0};
        /*使用一個pair來計算出中間過程中出現的字符個數, pair的功能比map要少一點適合於不用修改的數據*/
        Map<Character, Integer> times = new HashMap<>();
        while (r < charPos.size()){
            /*先是計算出包含着所有字母子串然後再對得到的子串進行縮小*/
            /*獲取當前出現字母的位置, pair的位置*/
            char c = charPos.get(r).getKey();
            /*當出現的字母的次數小於當前map對應的字母的個數說明還沒有湊夠還需要再找*/
            if (times.getOrDefault(c, 0) < charTimes.get(c)) cur++;
            /*下面一句代碼需要放到上一個判斷的後面*/
            times.put(c, times.getOrDefault(c, 0) + 1);
            /*從子串左端減少字符刪除一些不必要的字符, 進入這個循環表示l-r包含着所有的字符
            * 並且循環的條件爲刪除的字符是沒有用的對於構成最短的子串沒有什麼貢獻
            * */
            while (l <= r && cur == totalChars){
                int start = charPos.get(l).getValue();
                int end = charPos.get(r).getValue();
                if (ans[0] == -1 || end - start + 1 < ans[0]){
                    ans[0] = end - start + 1;
                    ans[1] = start;
                    ans[2] = end;
                }
                /*注意下面的是start而不是l*/
                char c1 = s.charAt(start);
                int curCharTimes = times.getOrDefault(c1, 0);
                ++l;
                times.put(c1, curCharTimes - 1);
                /*檢查刪除當前字符之後是否有影響*/
                if (times.get(c1) < charTimes.get(c1)) {
                    /*表明刪除當前的字符有影響因爲之前自增了l, 所以需要減去上一個t中含有的字母*/
                    --cur;
                    break;
                }
            }
            /*往右擴展*/
            ++r;
        }
        return ans[0] == -1 ? "" : s.substring(ans[1], ans[2] + 1);
    }
}

官方代碼:

class Solution {
  public String minWindow(String s, String t) {
      if (s.length() == 0 || t.length() == 0) {
          return "";
      }
      Map<Character, Integer> dictT = new HashMap<Character, Integer>();
      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 l = 0, r = 0;
      int formed = 0;
      Map<Character, Integer> windowCounts = new HashMap<Character, Integer>();
      int[] ans = {-1, 0, 0};
      while (r < s.length()) {
          char c = s.charAt(r);
          int count = windowCounts.getOrDefault(c, 0);
          windowCounts.put(c, count + 1);
          if (dictT.containsKey(c) && windowCounts.get(c).intValue() == dictT.get(c).intValue()) {
              formed++;
          }
          while (l <= r && formed == required) {
              c = s.charAt(l);
              // Save the smallest window until now.
              if (ans[0] == -1 || r - l + 1 < ans[0]) {
                  ans[0] = r - l + 1;
                  ans[1] = l;
                  ans[2] = r;
              }
              windowCounts.put(c, windowCounts.get(c) - 1);
              if (dictT.containsKey(c) && windowCounts.get(c).intValue() < dictT.get(c).intValue()) {
                  formed--;
              }
              l++;
          }
          r++;   
      }

      return ans[0] == -1 ? "" : s.substring(ans[1], ans[2] + 1);
  }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章