最小覆盖子串[困难]

优质博文:IT-BLOG-CN

最小覆盖子串[困难]_第1张图片

一、题目

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

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

示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 “BANC” 包含来自字符串tABC

示例 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
st由英文字母组成

进阶: 你能设计一个在o(m+n)时间内解决此问题的算法吗?

二、代码

思想: 我们用滑动窗口的思想解决这个问题。在滑动窗口类型的问题中都会有两个指针,一个用于「延伸」现有窗口的r指针,和一个用于「收缩」窗口的l指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在s上滑动窗口,通过移动r指针不断扩张窗口。当窗口包含t全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。

如何判断当前的窗口包含所有t所需的字符呢?我们可以用一个哈希表表示t中所有的字符以及它们的个数,用一个哈希表动态维护窗口中所有的字符以及它们的个数,如果这个动态表中包含t的哈希表中的所有字符,并且对应的个数都不小于t的哈希表中各个字符的个数,那么当前的窗口是「可行」的。

注意:这里t中可能出现重复的字符,所以我们要记录字符的个数。

优化: 如果s=XX⋯XABCXXXX,t=ABC,那么显然[XX⋯XABC]是第一个得到的「可行」区间,得到这个可行区间后,我们按照「收缩」窗口的原则更新左边界,得到最小区间。我们其实做了一些无用的操作,就是更新右边界的时候「延伸」进了很多无用的X,更新左边界的时候「收缩」扔掉了这些无用的X,做了这么多无用的操作,只是为了得到短短的ABC。没错,其实在s中,有的字符我们是不关心的,我们只关心t中出现的字符,我们可不可以先预处理s,扔掉那些t中没有出现的字符,然后再做滑动窗口呢?也许你会说,这样可能出现XXABXXC的情况,在统计长度的时候可以扔掉前两个X,但是不扔掉中间的X,怎样解决这个问题呢?优化后的时空复杂度又是多少?这里代码给出没有优化的版本。

class Solution {
    // 1、通过 hashMap 记录 t 中的字符串和个数
    // 2、通过 fast slow 快慢指针记录最短字符串的位置
    // 3、通过 hashMap 记录当前符合要求的字符串和个数
    Map<Character, Integer> ori = new HashMap();
    // 定义一个变量,保存字串的大小,并将符合要求的字串fast/slow指针赋值给resL,resR
    int fast = 0, slow = 0, len = Integer.MAX_VALUE, resL = -1, resR = -1;
    Map<Character, Integer> cur = new HashMap();

    public String minWindow(String s, String t) {
        // 4、将需要判断的字串维护在hashMap中
        for (int i = 0; i < t.length(); i++) {
            ori.put(t.charAt(i), ori.getOrDefault(t.charAt(i),0) + 1);
        }

        // 5、开始遍历s串,通过快慢指针
        while (fast < s.length() && slow <= fast) {
            // 6、将s逐个维护在hashMap中
            cur.put(s.charAt(fast), cur.getOrDefault(s.charAt(fast), 0) + 1);
            // 7、当新加入字符后,需要判断是否满足最小字串请求,并且小于之前字串的长度
            while (check(t.length())) {
                // left 还没有移动,所以下面的判断不能放在 while循环中
                if ((fast - slow + 1) < len) {
                    len = fast - slow + 1;
                    resL = slow;
                    resR = fast + 1;
                }
                // 将cur中slow下标的串-1
                cur.put(s.charAt(slow), cur.getOrDefault(s.charAt(slow), 0) -1);
                ++slow;
            }
            // 循环退出条件
            ++fast;
        }

        return resL == -1 ? "" : s.substring(resL, resR);
    }

    private boolean check(Integer len) {
        // 如果 fast 小于 t 的长度,直接返回 false
        if (fast < len - 1) {
            return false;
        }
        // 遍历 ori 或者 cur
        Iterator iterator = ori.entrySet().iterator();
        while (iterator.hasNext()) {
            // 如果 cur 包含该元素,val >= ori.val 则表示成功,否则失败;
            Map.Entry entry = (Map.Entry)iterator.next();
            Character key = (Character)entry.getKey();
            Integer val = (Integer)entry.getValue();
            // 当前返回的串的个数小于目标串t的个数,说明不符合,直接退出
            if (cur.getOrDefault(key, 0) < val) {
               return false;
            }
        }
        return true;
    }
}

时间复杂度: 最坏情况下左右指针对s的每个元素各遍历一遍,哈希表中对s中的每个元素各插入、删除一次,对t中的元素各插入一次。每次检查是否可行会遍历整个t的哈希表,哈希表的大小与字符集的大小有关,设字符集大小为C,则渐进时间复杂度为O(C⋅∣s∣+∣t∣))。
空间复杂度: 这里用了两张哈希表作为辅助空间,每张哈希表最多不会存放超过字符集大小的键值对,我们设字符集大小为C,则渐进空间复杂度为O(C)

你可能感兴趣的:(算法题,java,开发语言,算法,后端,数据结构,职场和发展,面试)