最小覆盖字串

题目:

给你一个字符串 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 由英文字母组成

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

分析

为了解决这个问题,我们可以使用滑动窗口的技巧。滑动窗口技术通常用于连续数据的问题,特别适合处理子字符串或子数组的问题。在这个问题中,我们将使用两个指针(left 和 right)来表示窗口的左右边界,通过移动这两个指针来探索所有可能的窗口,并找到包含字符串 t 的所有字符的最小窗口。

步骤

以下是解决问题的步骤:

  1. 初始化:使用两个哈希表,一个用来存储字符串 t 中每个字符的出现次数(need),另一个用来存储当前窗口中每个字符的出现次数(window)。同时,用两个整数变量 valid 来记录窗口中满足 t 中字符频率需求的字符数量,minLen 来记录最小覆盖子串的长度。

  2. 扩展右边界:移动 right 指针扩展窗口,直到窗口包含了 t 的所有字符。

  3. 收缩左边界:一旦找到一个有效的窗口,尝试通过移动 left 指针来缩小窗口大小,同时保持窗口仍然满足条件。

  4. 更新结果:在每次找到一个更小的有效窗口时,更新最小覆盖子串的起始位置和长度。

  5. 重复:重复步骤 2 和 3,直到 right 指针到达字符串 s 的末尾。

code

以下是 Java 代码实现:

class Solution {
    public String minWindow(String s, String t) {
        // 如果输入的字符串为空,则直接返回空字符串
        if (s.length() == 0 || t.length() == 0) return "";

        // 使用哈希表记录字符串 t 中每个字符的期望频率
        Map<Character, Integer> dictT = new HashMap<>();
        for (int i = 0; i < t.length(); i++) {
            char c = t.charAt(i);
            dictT.put(c, dictT.getOrDefault(c, 0) + 1);
        }

        // 记录必须满足的字符种类数(即 t 中不同字符的数量)
        int required = dictT.size();

        // 初始化左右指针
        int l = 0, r = 0;

        // 记录当前窗口中满足 t 的字符频率要求的字符种类数
        int formed = 0;

        // 用于记录当前窗口中各字符的实际频率
        Map<Character, Integer> windowCounts = new HashMap<>();

        // 用于记录最小覆盖子串的起始位置和长度
        int[] ans = {-1, 0, 0};

        while (r < s.length()) {
            // 从 s 中扩展右边界,添加一个字符至当前窗口
            char c = s.charAt(r);
            int count = windowCounts.getOrDefault(c, 0);
            windowCounts.put(c, count + 1);

            // 如果当前字符的频率满足 t 中的要求,则更新 formed 计数
            if (dictT.containsKey(c) && windowCounts.get(c).intValue() == dictT.get(c).intValue()) {
                formed++;
            }

            // 当窗口满足所有字符要求时,尝试通过移动左边界来缩小窗口大小
            while (l <= r && formed == required) {
                c = s.charAt(l);
                // 如果找到了更小的覆盖子串,则更新结果
                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);
    }
}

这段代码正确地实现了滑动窗口策略,并且能够在 O(m + n) 时间复杂度内解决问题,其中 m 是字符串 s 的长度,n 是字符串 t 的长度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值