LeetCode 0076 -- 最小覆盖子串

45 篇文章 0 订阅
12 篇文章 0 订阅

最小覆盖子串

题目描述

给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字母的最小子串。

示例:

输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"

说明:

  • 如果 S 中不存这样的子串,则返回空字符串 ""
  • 如果 S 中存在这样的子串,我们保证它是唯一的答案。

解题思路

个人AC

理解有偏差,没有AC。单纯地以为T中的字母不会有重复,只需要用HashMap来记录每个字符出现的位置即可。

class Solution {
    public String minWindow(String s, String t) {
        HashMap<Character, Integer> window = new HashMap() {{
            for (int i = 0; i < t.length(); i++) {
                this.put(t.charAt(i), -1);
            }
        }};
        HashMap<Character, Integer> target = new HashMap() {{
            for (int i = 0; i < t.length(); i++) {
                this.put(t.charAt(i), -1);
            }
        }};
        
        int minSum = Integer.MAX_VALUE;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (!window.containsKey(c)) continue;
            window.put(c, i);

            int sum = calDiffAbsSum(window);
            if (sum < 0) continue;
            if (sum < minSum) {
                for (Character key : window.keySet()) {
                    target.put((key), window.get(key));
                }
            }
        }
        int start = Integer.MAX_VALUE, end = Integer.MIN_VALUE;
        for (int v : target.values()) {
            if (v < 0) return "";
            if (v < start) start = v;
            if (v > end) end = v;
        }
        return s.substring(start, end + 1);
    }

    // 计算两两差值绝对值的和
    private int calDiffAbsSum(HashMap<Character, Integer> window) {
        int sum = 0;
        for (int a : window.values()) {
            if (a < 0) return -1;
            for (int b : window.values()) {
                if (b < 0) return -1;

                if (a != b) {
                    sum += Math.abs(a - b);
                }
            }
        }
        return sum / 2;
    }
}

执行结果:解答错误

输入:

"a"
"aa"

输出:

"a"

预期结果:

""

最优解

滑动窗口

参考自:leetcode-cn题解 滑动窗口算法通用思想

思路:

  1. 在字符串s中使用双指针中的左右指针技巧,初始化left = right = 0,把索引区间[left, right]称为一个窗口;
  2. 先不断地向右移动right指针,扩大窗口,直到窗口中的字符串符合要求(包含了t中的所有字符);
  3. 此时,停止移动right,转而不断移动left指针缩小窗口,直到窗口中的字符串不再符合要求(不包含t中的所有字符),每次增加left时,都要更新一轮内容;
  4. 重复第23步,直到right到达字符串s的末尾。

本质:第2步相当于寻找一个“可行解”,第3步相当于优化这个“可行解“,最终找到最优解。


needswindow相当于计数器,分别记录t中字符出现次数和窗口中的相应字符的出现次数。

初始状态:

在这里插入图片描述

移动right,直到窗口[left, right]包含了t中所有字符:

在这里插入图片描述

然后移动left,缩小窗口[left, right]

在这里插入图片描述

直到窗口中的字符串不再符合要求,left不再继续移动:

在这里插入图片描述

之后重复上述过程,先移动right,再移动left…… 直到right指针到达字符串s的末尾,算法结束。

上述过程可以简单地写出如下伪码框架:

string s, t;
// 在 s 中寻找 t 的「最小覆盖子串」
int left = 0, right = 0;
string res = s;

while(right < s.size()) {
    window.add(s[right]);
    right++;
    // 如果符合要求,移动 left 缩小窗口
    while (window 符合要求) {
        // 如果这个窗口的子串更短,则更新 res
        res = minLen(res, window);
        window.remove(s[left]);
        left++;
    }
}
return res;

如何判断window即子串s[left:right+1]是否包含t的所有字符呢?

可以用两个哈希表当作计数器解决。用一个哈希表needs记录字符串t中包含的字符及出现次数,用另一个哈希表window记录当前窗口[left, right]中包含的字符及出现的次数,如果window包含所有needs中的键,且这些键对应的值都大于等于needs中的值,那么就可以知道当前窗口[left, right]符合要求了,然后可以移动left指针了。

class Solution {
    public String minWindow(String s, String t) {
        // 记录最短子串的开始位置和长度,即覆盖最小子串的窗口
        int minLeft = 0, minLen = Integer.MAX_VALUE;
        // 当前窗口
        int left = 0, right = 0;
        
    	HashMap<Character, Integer> needs = new HashMap<Character, Integer>() {{
           for (int i = 0; i < t.length(); i++) {
               char key = t.charAt(i);
               this.put(key, this.getOrDefault(key, 0) + 1);
           }
        }};
        HashMap<Character, Integer> window = new HashMap<>();
        
        int match = 0;
        while (right < s.length()) {
            char c = s.charAt(right);
            if (needs.containsKey(c)) {
                window.put(c, window.getOrDefault(c, 0) + 1);
                // 常量池 -128 ~ 127,若不手动拆箱,当字符串过长时会出现问题
                if (window.get(c).intValue() == needs.get(c).intValue()) {
                    match++;
                }
            }
            right++;
            
            // 不断移动right指针,直到找到包含t中所有字母的子串
            // 然后移动左指针,优化当前解
            while (match == needs.size()) {
                if (right - left < minLen) {
                    // 更新最小子串的开始位置和长度
                    minLeft = left;
                    minLen = right - left;
                }
                
                c = s.charAt(left);
                if (needs.containsKey(c)) {
                    window.put(c, window.get(c) - 1);
                    if (window.get(c) < needs.get(c)) {
                        // 字符c的出现次数不再符合要求
                        match--;
                    }
                }
                left++;
            }
        }
        return minLen == Integer.MAX_VALUE ? "" : s.substring(minLeft, minLeft + minLen);
    }
}

时间复杂度: O ( m + n ) O(m + n) O(m+n),其中mn分别是字符串st的长度;

空间复杂度: O ( m ) O(m) O(m)

举一反三

  1. LeetCode 0003 – 无重复字符的最长子串
  2. LeetCode 0438 – 找到字符串中所有字母异位词
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值