力扣经典题目解析--最小覆盖子串

文章讲述了如何在给定字符串s和目标字符串t的情况下,利用滑动窗口算法高效地找到s中包含t所有字符的最小子串。原始暴力法被改进为滑动窗口方法,逐步优化字符频率检查,降低时间复杂度。

跟随虾哥项目实践,硬件选小智就对了

xiaozhi 开源方案官方适配,二次开发文档齐全

原题地址: . - 力扣(LeetCode)

给你一个字符串 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 的子串中,
因此没有符合条件的子字符串,返回空字符串。

暴力法

暴力法就是遍历所有子串,然后统计子串中每个字符出现的次数,然后跟t中每个字符出现的次数做比较,如果包含t中所有字符并且字符出现的次数大于等于t,则说明该子串符合条件,找出所有满足条件的子串,通过比较得到最小子串

public String minWindow(String s, String t) {
    // 保存自小子串
    String result = "";
    // 统计t中字符串数量
    Map<Character, Integer> tCharStatis = statis(t);
    // 遍历s
    for (int i = 0; i < s.length(); i++) {
        // 遍历子串,子串范围为[i,j)
        for (int j = i + t.length(); j <= s.length(); j++) {
            String subStr = s.substring(i, j);
            Map<Character, Integer> subCharStatis = statis(subStr);
            // 如果子串包含t,则判断是否为最小子串
            if (check(subCharStatis, tCharStatis)) {
                if ("".equals(result) || subStr.length() < result.length()) {
                    result = subStr;
                }
            }
        }
    }
    return result;
}

// 统计字符串中单个字符数量
private Map<Character, Integer> statis(String str) {
    Map<Character, Integer> map = new HashMap<>();
    for (int i = 0; i < str.length(); i++) {
        int count = map.getOrDefault(str.charAt(i), 0);
        count++;
        map.put(str.charAt(i), count);
    }
    return map;
}

// 判断子串是否包含t
private boolean check(Map<Character, Integer> subStatis, Map<Character, Integer> tStatis) {
    for (Character c : tStatis.keySet()) {
        int subCount = subStatis.getOrDefault(c, 0);
        if (subCount < tStatis.get(c)) return false;
    }
    return true;
}

效果:

代码没问题,但是时间复杂度太高,没法满足需求 

滑动窗口

暴力法的缺点是显而易见的:时间复杂度过大,超出了运行时间限制。在哪些方面可以优化呢?

仔细观察可以发现,我们在暴力求解的时候,做了很多无用的比对:对于字符串“ADOBECODEBANC”,当找到一个符合条件的子串“ADOBEC”后,我们会继续仍以“A”作为起点扩展这个子串,得到一个符合条件的“ADOBECO”——它肯定符合条件,也肯定比之前的子串长,这其实是完全不必要的。

代码实现上,我们可以定义两个指针:指向子串“起始点”的左指针,和指向子串“结束点”的右指针。它们一个固定、另一个移动,彼此交替向右移动,就好像开了一个大小可变的窗口、在不停向右滑动一样,所以这就是非常经典的滑动窗口解决问题的应用场景。所以有时候,滑动窗口也可以归类到双指针法。

public String minWindow1(String s, String t) {
    // 保存自小子串
    String result = "";
    // 统计t中字符串数量
    Map<Character, Integer> tCharStatis = statis(t);
    int lp = 0;
    int rp = t.length();
    while (rp <= s.length()) {
        String subStr = s.substring(lp, rp);
        Map<Character, Integer> subCharStatis = statis(subStr);
        // 子串符合条件,则左指针右移
        if (check(subCharStatis, tCharStatis)) {
            if ("".equals(result) || subStr.length() < result.length()) {
                result = subStr;
            }
            lp++;
        } else {
            //不符合条件则右指针右移继续寻找
            rp++;
        }
    }
    return result;
}

// 统计字符串中单个字符数量
private Map<Character, Integer> statis(String str) {
    Map<Character, Integer> map = new HashMap<>();
    for (int i = 0; i < str.length(); i++) {
        int count = map.getOrDefault(str.charAt(i), 0);
        count++;
        map.put(str.charAt(i), count);
    }
    return map;
}

// 判断子串是否包含t
private boolean check(Map<Character, Integer> subStatis, Map<Character, Integer> tStatis) {
    for (Character c : tStatis.keySet()) {
        int subCount = subStatis.getOrDefault(c, 0);
        if (subCount < tStatis.get(c)) return false;
    }
    return true;
}

效果:

比之前有进步,但是还需进一步优化 

滑动窗口优化

我们判断S是否满足包含T中所有字符的时候,调用的方法check其实又是一个暴力法:遍历T中所有字符频次,一一比对。上面的复杂度分析也可以看出,遍历s只用了线性时间,但每次都要遍历一遍T的频次哈希表,这就耗费了大量时间。

我们已经知道,每次指针的移动,只涉及到一个字符的增减。所以我们其实不需要知道完整的频次HashMap,只要获取改变的这个字符的频次,然后再和T中的频次比较,就可以知道新子串是否符合要求了。

public String minWindow(String s, String t) {
    // 保存最小子串
    String result = "";
    // 统计t中字符串数量
    Map<Character, Integer> tCharStatis = statis(t);
    Map<Character, Integer> subCharStatis = new HashMap<>();
    int lp = 0;
    int rp = 1;
    while (rp <= s.length()) {
        char newChar = s.charAt(rp - 1);
        // t中包含该字符再统计
        if (tCharStatis.containsKey(newChar)) {
            int count = subCharStatis.getOrDefault(newChar, 0);
            subCharStatis.put(newChar, count + 1);
        }
        // 子串符合条件,则左指针右移
        while (check(subCharStatis, tCharStatis) && lp < rp) {
            if ("".equals(result) || rp - lp < result.length()) {
                result = s.substring(lp, rp);
            }
            char removedChar = s.charAt(lp);
            if (tCharStatis.containsKey(removedChar)) {
                subCharStatis.put(removedChar, subCharStatis.getOrDefault(removedChar, 0) - 1);
            }
            lp++;
        }
        rp++;
    }
    return result;
}

// 统计字符串中单个字符数量
private Map<Character, Integer> statis(String str) {
    Map<Character, Integer> map = new HashMap<>();
    for (int i = 0; i < str.length(); i++) {
        int count = map.getOrDefault(str.charAt(i), 0);
        count++;
        map.put(str.charAt(i), count);
    }
    return map;
}

// 判断子串是否包含t
private boolean check(Map<Character, Integer> subStatis, Map<Character, Integer> tStatis) {
    for (Character c : tStatis.keySet()) {
        int subCount = subStatis.getOrDefault(c, 0);
        if (subCount < tStatis.get(c)) return false;
    }
    return true;
}

跟随虾哥项目实践,硬件选小智就对了

xiaozhi 开源方案官方适配,二次开发文档齐全

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值