时间复杂度O(n)查找最小覆盖子串

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

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

用暴力解法的时间复杂度是O(n^2),我这里使用滑动窗口法实现的时间复杂度是O(n)

核心思路:滑动窗口(双指针) 结合 哈希表。通过维护一个动态窗口(由左右指针界定),逐步扩大和缩小窗口以寻找满足条件的最小子串。

  1. 统计目标字符需求:用哈希表 tmap 记录 t 中每个字符的出现次数(键为字符,值为次数),并记录 t 中不同字符的种类数。
  2. 维护窗口字符统计:用哈希表 smap 记录当前窗口中每个字符的出现次数。
  3. 动态调整窗口:
    移动右指针扩大窗口,直到窗口包含 t 中所有字符(且数量满足需求)。
    当窗口满足条件时,移动左指针缩小窗口,尝试在保持条件的前提下找到更小的子串。
  4. 判断窗口有效性:用变量 valid 记录窗口中满足要求的字符种类数,当 valid 等于 t 的字符种类数时,窗口有效。
class Solution {
    public String minWindow(String s, String t) {
       
        Map<Character, Integer> smap = new HashMap<>(); //当前窗口
        Map<Character, Integer> tmap = new HashMap<>();
        int left = 0, right = 0;       // 左右指针
        int valid = 0;    // 满足频率条件的字符数量(统计有效字符),比如当smap中A的数量达到3个时,valid才会把A当做有效字符,valid加一
        int start = 0, minLen = Integer.MAX_VALUE; // 最小子串的起始位置和长度
        int slength = s.length();

        
        // 处理边界情况
        if (s == null || t == null || s.length() == 0 || t.length() == 0 || s.length() < t.length()) {
            return "";
        }
        
        // 统计t中每个字符出现的次数
        for (char c : t.toCharArray()) {
            tmap.put(c, tmap.getOrDefault(c, 0) + 1);
        }
        int tmap_size = tmap.size(); // t中字符种类数 
        
        /** 
            遍历s字符串
         */
        while (right < slength) {
            char c = s.charAt(right);            
            
            // 判断是不是目标字符
            if (tmap.containsKey(c)) {
                smap.put(c, smap.getOrDefault(c, 0) + 1); //更新目标字符的频率
                if (smap.get(c).intValue() == tmap.get(c).intValue()) {  // 如果频率相同了,成为有效字符,加入字符计数器
                    valid++;
                }
            }
            
            /** 判断是不是子串。
                是子串则要更新长度,然后缩小窗口(用左指针遍历当前窗口)
            */ 
            while (valid == tmap_size) { //有效字符数量与t相等
                // 如果当前子串长度更小,则更新最小子串索引和长度
                if (right - left+1 < minLen) {
                    start = left;
                    minLen = right - left+1;
                }
                
                /** 逐步缩小窗口,移除左指针的字符               
                */ 
                char d = s.charAt(left);
                // 如果左指针元素是有效元素,则在smap中移除d字符
                if (tmap.containsKey(d)) {
                    smap.put(d, smap.get(d) - 1); 
                   
                    // 移除字符 d 后,判断是否要更新有效字符数。比较smap与tmap中的d数量,如果d字符数量变少了,说明d是有效字符,所以移除d后,有效值需要减 1。
                    if(smap.get(d).intValue() < tmap.get(d).intValue() ){
                        valid--;
                    }                 
                }

                left++;
            }
            right++;
        }
        
        return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen); // 如果没有找到最小字符串则返回空字符串。否则返回 从索引start到索引start+minLen(不包含!!)
    }
}
posted @ 2025-07-20 23:00  junjunyi  阅读(38)  评论(0)    收藏  举报