LeetCode.76-最小子串

96 阅读5分钟

解题随记

题目

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

 

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

 

示例 1:

输入: s = "ADOBECODEBANC", t = "ABC"
输出: "BANC"
解释: 最小覆盖子串 "BANC" 包含来自字符串 t 的 'A''B''C'

示例 2:

输入: s = "a", t = "a"
输出: "a"
解释: 整个字符串 s 是最小覆盖子串。

示例 3:

输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

 

提示:

  • m == s.length
  • n == t.length
  • 1 <= m, n <= 105
  • s 和 t 由英文字母组成

思路

抓重点

  • 如何保障target的所有字符都在窗口内。
  • 重复字符的判断,比如BBAC
  • 多个满足条件的字符串,如果判断最短

解题方法

  • 通过一个数据结构记录目标字符串target中字符对应出现次数(可以用map或者数组),目的是用来判断当前字符串是否包含target中全部字符
  • 遍历源字符串originStr
  • 如果当前字符存在于目标字符串target当中,则相应记录数-1,并记录该字段在源字符串originStr中的下标索引位置(不存在则不处理如果判断map中所有key对应value为0不好控制的话,可以使用target.length判断,注意当出现连续重复的只需要减一次即可)
  • 当出现满足目标字符串target的子串时,记录head和last(源数据位置);
  • 将窗口向右移一位,并重复判断当前字符串能否覆盖目标字符串target,如果不能则右指针往右移一位,如果能则判断当前窗口字符串是否比上次窗口字符串的长度更短,是则覆盖上次记录,否则跳过。

76_fig1.gif

附代码:

public static String minWindow(String s, String t) {
    // 特判,当s长度小于t时,不可能存在改情况数据,返回空串
    if (s.length() < t.length()) {
        return "";
    }
    
    // 得到目标字符串每个字段的出现次数(此处使用两倍空间,主要为了使用后半段判断是否存在,前半段控制加减)
    int[] ints = new int[256];
    for (int i = 0; i < t.length(); i++) {
        ints[t.charAt(i)] ++;
        ints[128 + t.charAt(i)] ++;
    }
    
    // 结果:s对应下标索引,0-head|1-last
    int[] result = new int[2];
    
    int sLength = s.length();
    // 用来判断是否覆盖
    int tLength = t.length();
    // 用来根据索引获取原来字符串对应位置的char值
    char[] sCharArray = s.toCharArray();
    // 存放原字符串中存在于目标字符串的char下标,通过出队入队获取到最终的head和last值
    Deque<Integer> deque = new ArrayDeque<>();
    for (int i = 0; i < s.length(); i++) {
        // 当前值
        Character c = s.charAt(i);
        
        // 存在于目标字符串中
        if (ints[128 + c] > 0) {
            // 加入到deque末尾,如果加入到队首,会导致后续出队无效
            deque.addLast(i);
            // 用完一个减掉一个(这里可能有负数,类似BBBAC)
            ints[c] --;
            // 因为上面有负数,所以只有正常情况(负数都是重复出现)才能算作有效字段
            if (ints[c] >= 0) {
                tLength--;
            }
        }
        
        // 代表出现覆盖子串,t中所有char都出现了
        if (tLength == 0) {
            
            // 处理map中负数,也就是类似BBBAC这样的重复数
            while (!deque.isEmpty() && ints[sCharArray[deque.peekFirst()]] < 0) {
                Integer currentDequeFirstIndex = deque.removeFirst();
                ints[sCharArray[currentDequeFirstIndex]] ++;
            }
            // 到现在deque中就没有重复的char了,我们就要开始计算head和last了

            // 我们是按顺序遍历的,所有队首就是头,最小的起始值
            int head = deque.removeFirst();
            // 控制只有小的才会重新计算head和last,比如当前计算的sLength大于上一次的值,则忽略
            if (i - head + 1 <= sLength) {
                sLength = i - head + 1;
                result[0] = head;
                // i+1是为了后续subString好处理点,也可以选择此处不加1,后续结果的的时候再+1
                result[1] = i + 1;
            }
            // 有值出队了,自然空的字符数要加上,相应的判断覆盖条件的也要+1,保持平衡
            ints[sCharArray[head]] ++;
            tLength++;
            
        }
        
    }
    // 按照head和last截取即可
    return s.substring(result[0], result[1]);

}

Tips:

  1. 使用Map + targetLength做加减法判断当前窗口字符串是否包含target中全部字符,当targetLength==0即全部覆盖了。
  2. 使用Deque按顺序从小到大记录target中字符在源字符串中出现的位置下标,便于滑动窗口。
  3. 考虑BBCA这种场景,Map中对应字符可能出现负数,所以当全部覆盖场景下,需要先处理下前缀连续重复的数据,连续重复字符保留一个即可。
  4. 发现满足条件的子串后,需要比较当前窗口的子串长度和之前子串长度,保留较短的那个.
  5. 因为前面已经处理了连续重复的数据,当队首的字符(符合条件)出队以后,当前窗口字符串一定是不满足所有target字符都在其中,然后遍历的过程就相当于窗口不停向右扩展的一个过程;当再次碰到符合条件的字符串后,比较字符串head和last之前的长度,再次将队首出队,模拟窗口向左收缩的过程.