LeetCode Top 100 Liked Questions 76. Minimum Window Substring (Java版; Hard)

welcome to my blog

LeetCode Top 100 Liked Questions 76. Minimum Window Substring (Java版; Hard)

题目描述
Given a string S and a string T, find the minimum window in S which will contain all the characters in T in complexity O(n).

Example:

Input: S = "ADOBECODEBANC", T = "ABC"
Output: "BANC"
Note:

If there is no such window in S that covers all characters in T, return the empty string "".
If there is such window, you are guaranteed that there will always be only one unique minimum window in S.
class Solution {
    public String minWindow(String s, String t) {
        int n = s.length(), m = t.length();
        if(n<m){
            return "";
        }
        HashMap<Character, Integer> map = new HashMap<>();
        for(char ch : t.toCharArray()){
            map.put(ch, map.getOrDefault(ch, 0)-1);
        }
        int left=0, right=0;
        int len = 0;
        int[] tmp = {-1, -1};
        while(right<n){
            char ch = s.charAt(right);
            if(map.containsKey(ch)){
                map.put(ch, map.get(ch)+1);
                if(map.get(ch)==0){
                    len++;
                }
            }
            while(len==map.size()){
                if(tmp[0]==-1 || right-left+1<tmp[1]){
                    tmp[0] = left;
                    tmp[1] = right - left + 1;
                }
                char ch2 = s.charAt(left);
                if(map.containsKey(ch2)){
                    map.put(ch2, map.get(ch2)-1);
                    if(map.get(ch2)<0){
                        len--;
                    }
                }
                left++;
            }
            right++;
        }
        return tmp[0]==-1? "" : s.substring(tmp[0], tmp[0]+tmp[1]);
    }
}
第二次做; 核心: 1) 双指针之滑动窗口 2)在缩小窗口的循环中更新窗口 3)记录的是窗口的大小,左边界和右边界, 最后再转成字符串 4)Integer与==, 用.intValue()方法对[-128,127]范围外的整数进行拆箱 5) 如何判断map中包含t中的所有字符串? 使用count
/*
双指针:都是从左往右
外循环:
    内循环: 移动right, 直至满足条件
    满足条件时; 内循环: 移动left, 直至不满足条件
    
细节:
如何检查当前窗口中是否包含t的所有字符? O(1)时间复杂度
*/
class Solution {
    public String minWindow(String s, String t) {
        if(s==null || s.length()==0 || t==null || t.length()==0)
            return "";
        HashMap<Character, Integer> countMap = new HashMap<>();
        HashMap<Character, Integer> map = new HashMap<>();
        for(int i=0; i<t.length(); i++){
            char ch = t.charAt(i);
            countMap.put(ch, countMap.getOrDefault(ch,0)+1);            
        }
        //map中key的个数
        int count = 0;
        //双指针, 滑动窗口
        int left=0, right=0;
        //可行窗的信息:长度, 起始索引, 终止索引
        int[] window = {-1, -1, -1};
        while(right < s.length()){
            char ch = s.charAt(right);
            if(countMap.containsKey(ch)){
                map.put(ch, map.getOrDefault(ch,0)+1);
                //核心: Integer与==
                //核心: 只在相等的时候更新count; 大于的时候不更新
                if(map.get(ch).intValue() == countMap.get(ch).intValue()){
                    count++;
                }
            }
            
            //当前窗为可行窗, 缩小可行窗, 直至不可行
            while(count==countMap.size() && left <= right){
                //更新window
                if(window[0]==-1 || (right-left+1) < window[0]){
                    window[0] = right - left + 1;
                    window[1] = left;
                    window[2] = right;
                }
                //更新map
                char ch2 = s.charAt(left);
                if(map.containsKey(ch2)){
                    map.put(ch2, map.get(ch2)-1);
                    //更新count
                    if(map.get(ch2) < countMap.get(ch2)){
                        count--;
                    }
                }
                //缩小窗口
                left++;
                
            }
            //update
            right++;
        }
        return window[0]==-1? "" : s.substring(window[1], window[2]+1);
    }
}
第一次做, 双指针围成的滑动窗口; 看了题解之后使用两个哈希表求解, 优化了两个地方: 1)判断s.substring(left, right+1)是否包含T中的每个字符时更快了; 2)存储的是最短字符串的相关索引, 并没有每次创建一个新的字符串; 使用了JDK8中的新函数, hm.getOrDefault(); 最奇怪的地方是if(countMap.containsKey(s.charAt(right)) && currMap.get(s.charAt(right)).intValue() == countMap.get(s.charAt(right)).intValue())中必须要加.intValue(), 否则倒数第二个案例不能通过, 但是if(countMap.containsKey(s.charAt(left)) && currMap.get(s.charAt(left)) + 1 == countMap.get(s.charAt(left)))不用加.intValue(), 这是为什么? 因为==比较的是对象的内存地址, 并不是int值, 所以用intValue将其转成int类型的值, 不过如果对应的值在[-128,127]这个范围上时, 会自动拆箱为int类型; 第二个案例之所以能通过, 是因为+1之后自动拆箱了, 由Integer变成int
符合题目要求的滑动窗口叫作可行窗
双指针的功能: 右指针延伸现有窗口; 左指针收缩现有窗口
同一时刻只有一个指针在移动

通过移动右指针延伸现有窗口, 直到找到可行窗
通过移动左指针收缩现有窗口, 循环最短的可行窗
import java.util.HashMap;

class Solution {
    public String minWindow(String s, String t) {
        String res = "";
        if(s==null || t==null || s.length()==0 || t.length()==0)
            return res;
        //
        HashMap<Character, Integer> countMap = new HashMap<>();
        HashMap<Character, Integer> currMap = new HashMap<>();
        //initialize
        int left = 0, right = 0;
        for(int i=0; i<t.length(); i++){
            int count = countMap.getOrDefault(t.charAt(i), 0);
            countMap.put(t.charAt(i), count+1);
        }
        //用来检查当前窗口是否包含T中所有的字符; 不需要再每次遍历哈希表进行判断
        int trace = 0;
        int finalTrace = countMap.size();
        //可行窗中元素的个数和索引
        int[] windowRange = {-1, 0, 0};
        while(right < s.length()){
            //execute
            //加入字符
            int count = currMap.getOrDefault(s.charAt(right), 0);
            currMap.put(s.charAt(right), count+1);
            //是否更新trace
            if(countMap.containsKey(s.charAt(right)) && currMap.get(s.charAt(right)).intValue() == countMap.get(s.charAt(right)).intValue())
                trace++;
            //判断现有窗口是否为可行窗, 需要重复判断
            //内部循环要注意索引关系, 左指针不能超过右指针
            while(left <= right && trace==finalTrace){
                if(windowRange[0] == -1 || windowRange[0] > (right - left + 1)){
                    windowRange[0] = right - left + 1;
                    windowRange[1] = left;
                    windowRange[2] = right;
                }
                currMap.put(s.charAt(left), currMap.get(s.charAt(left)) - 1);
                //注意trace不是每次都更新, 对于同一个字符, trace只更新一次
                //这里写成if (dictT.containsKey(c) && windowCounts.get(c).intValue() <dictT.get(c).intValue())也行 , 因为在内层的while循环中, trace--一次之后就跳出循环了, 不会出现我担心的连续trace--
                if(countMap.containsKey(s.charAt(left)) && currMap.get(s.charAt(left)) + 1 == countMap.get(s.charAt(left)))
                        trace--;
                //update
                left++;
            }
            //update
            right++;
        }
        return windowRange[0] == -1 ? "" : s.substring(windowRange[1], windowRange[2]+1);
    }
}
第一次做, 双指针; 只用一个哈希表即可; 初始化时用哈希表记录T中各个字符出现的次数, 遍历s, 向右移动right, 如果遇到了T中的字符, 则哈希表中该字符对应的出现次数减1; 如何判断s.substring(left, right+1)包含T中的每一个字符(重复字符要考虑重复次数)?
遍历哈希表, 
如果哈希表中的entry对应的value大于0, 表示s.substring(left, right+1)没有完全包含entry对应的字符, 要么是没有遇到该字符, 要么是遇到的次数还不够
如果哈希表中的entry对应的value等于0, 表示s.substring(left, right+1)含有entry对应的字符(重复的次数也考虑在内了); 
如果哈希表中的entry对应的value小于0, 表示s.substring(left, right+1)含有entry对应的字符(重复的次数也考虑在内了), 而且还额外遇到了几次entry对应的字符;
/*
非常规例子
"a"
"aa"
*/
import java.util.HashMap;
import java.util.Map;

class Solution {
    public String minWindow(String s, String t) {
        String res = "";
        if(s==null || t==null || s.length()==0 || t.length()==0)
            return res;
        //
        int left = 0, right = 0;
        HashMap<Character, Integer> countMap = new HashMap<>();
        for(int i=0; i<t.length(); i++){
            if(!countMap.containsKey(t.charAt(i)))
                countMap.put(t.charAt(i), 1);
            else
                countMap.put(t.charAt(i), countMap.get(t.charAt(i))+1);
        }

        while(left<s.length() && right<s.length()){
            //加入一个字符
            if(countMap.containsKey(s.charAt(right)))
                countMap.put(s.charAt(right), countMap.get(s.charAt(right))-1);

            //包含了T中的所有字符, 缩小左边界; 这里需要重复判断
            while(containsAllT(countMap)){
                if(res.equals(""))
                    res = s.substring(left, right+1);
                else
                    res = res.length() < (right - left + 1) ? res : s.substring(left, right+1);
                //update
                if(countMap.containsKey(s.charAt(left)))
                    countMap.put(s.charAt(left), countMap.get(s.charAt(left))+1);
                left++;
            }
            //update
            right++;
        }
        return res;
    }
    public boolean containsAllT(HashMap<Character, Integer> countMap){
        for(Map.Entry<Character, Integer> entry : countMap.entrySet()){
            if(entry.getValue() > 0)
                return false;
        }
        return true;
    }
}
题解分析, 虽然用了两个哈希表, 但是比我的方法快
称包含T中所有字符的窗口为可行窗口。 可以用双指针围成的滑动窗口法来解决本问题。

在滑动窗口类型的问题中都会有两个指针。一个用于延伸现有窗口的right指针,和一个用于收缩窗口的left 指针。在任意时刻,只有一个指针运动,而另一个保持静止。

本题的解法很符合直觉。我们通过移动right指针不断扩张窗口。当窗口包含全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。

最终答案是最小的可行窗口。
class Solution {
  public String minWindow(String s, String t) {

      if (s.length() == 0 || t.length() == 0) {
          return "";
      }

      // Dictionary which keeps a count of all the unique characters in t.
      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);
      }

      // Number of unique characters in t, which need to be present in the desired window.
      int required = dictT.size();

      // Left and Right pointer
      int l = 0, r = 0;

      // formed is used to keep track of how many unique characters in t
      // are present in the current window in its desired frequency.
      // e.g. if t is "AABC" then the window must have two A's, one B and one C.
      // Thus formed would be = 3 when all these conditions are met.
      int formed = 0;

      // Dictionary which keeps a count of all the unique characters in the current window.
      Map<Character, Integer> windowCounts = new HashMap<Character, Integer>();

      // ans list of the form (window length, left, right)
      int[] ans = {-1, 0, 0};

      while (r < s.length()) {
          // Add one character from the right to the window
          char c = s.charAt(r);
          int count = windowCounts.getOrDefault(c, 0);
          windowCounts.put(c, count + 1);

          // If the frequency of the current character added equals to the
          // desired count in t then increment the formed count by 1.
          if (dictT.containsKey(c) && windowCounts.get(c).intValue() == dictT.get(c).intValue()) {
              formed++;
          }

          // Try and co***act the window till the point where it ceases to be 'desirable'.
          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;
              }

              // The character at the position pointed by the
              // `Left` pointer is no longer a part of the window.
              windowCounts.put(c, windowCounts.get(c) - 1);
              if (dictT.containsKey(c) && windowCounts.get(c).intValue() < dictT.get(c).intValue()) {
                  formed--;
              }

              // Move the left pointer ahead, this would help to look for a new window.
              l++;
          }

          // Keep expanding the window once we are done co***acting.
          r++;   
      }

      return ans[0] == -1 ? "" : s.substring(ans[1], ans[2] + 1);
  }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值