12.最小覆盖子串

68 阅读2分钟

竟然有一个月没刷题了。。放完假咯,可以继续学习了~

题目链接

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

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"

输出:"BANC"

解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。

解法1 暴力解法

思路

首先遍历 s 的每一个起始位置 i。从 i 开始扩大子串,直到 j。对于每个子串 s[i:j] 都检查是否包含字符串 t 的所有字符,如果存在,则更新最短的长度和字符串。

代码

function minWindow(s: string, t: string): string {
    if (!s.length || !t.length) {
        return "";
    }

    const containAllChars = (substr, tFreq):boolean => {
        const subStrFreq: { [key: string]: number } = {};
        for (const char of substr) {
            subStrFreq[char] = (subStrFreq[char] || 0) + 1;
        }

        for (const char in tFreq) {
            if (!subStrFreq[char] || subStrFreq[char] < tFreq[char]) {
                return false;
            }
        }
        return true;
    }

    const tFreq: { [key: string]: number } = {};
    for (const char of t) {
        tFreq[char] = (tFreq[char] || 0) + 1;
    }

    let minLen: number = Infinity;
    let minSubStr: string = "";

    for (let i = 0; i < s.length; i++) {
        for (let j = i + 1; j <= s.length; j++) {
            const subStr = s.substring(i, j);
            if (containAllChars(subStr, tFreq)) {
                if (subStr.length < minLen) {
                    minLen = subStr.length;
                    minSubStr = subStr;
                }
            }
        }
    }
    return minSubStr;
};

时空复杂度分析

时间复杂度:两层遍历枚举 i, j 时是O(n^2),加上检查该字符串所需要时间,所以是 O(n^3)

空间复杂度:O(n),用于存储子串的字符频率表

解法2 滑动窗口

代码

function minWindow(s: string, t: string): string {
    if (!s.length || !t.length) {
        return "";
    }

    const tFreq: { [key: string]: number } = {};
    for (const char of t) {
        tFreq[char] = (tFreq[char] || 0) + 1;
    }
    
    // 左右指针、已匹配的字符数、窗口内字符频率
    let left = 0, right = 0;
    let required = Object.keys(tFreq).length;
    let formed = 0;
    const windowCounts: { [key: string]: number } = {};

    // 用于记录最小窗口的起始位置和长度
    let minLen = Infinity;
    let minLeft = 0;

    while (right < s.length) {
        const char = s[right];
        windowCounts[char] = (windowCounts[char] || 0) + 1;

        // 如果当前字符的频率满足 t 中的要求,增加 formed 计数
        if (tFreq[char] && windowCounts[char] === tFreq[char]) {
            formed++;
        }

        // 尝试缩小窗口
        while (left <= right && formed === required) {
            const currentChar = s[left];

            // 更新最小子串
            if (right - left + 1 < minLen) {
                minLen = right - left + 1;
                minLeft = left;
            }

            // 移除窗口最左侧的字符,并更新频率
            windowCounts[currentChar]--;
            if (tFreq[currentChar] && windowCounts[currentChar] < tFreq[currentChar]) {
                formed--;
            }

            left++;
        }

        // 继续扩大窗口
        right++;
    }

    return minLen === Infinity ? "" : s.substring(minLeft, minLeft + minLen);
};

时空复杂度

时间复杂度:O(|t| + |s|) , |t| 代表 t 的长度

空间复杂度:O(|t| + |s|)