LeetCode —— 76. 最小覆盖子串

104 阅读4分钟

启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第12天,点击查看活动详情

该题是数组长度最小的子数组题型第三题。

题目来源

76. 最小覆盖子串 - 力扣(LeetCode)

题目描述(困难

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

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例1

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

示例2

输入: s = "a", t = "a"
输出: "a"

示例3

输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

提示

  • 1<=s.length,t.length<=1051 <= s.length, t.length <= 10^5
  • s 和 t 由英文字母组成

进阶: 你能设计一个在 O(n)O(n) 时间内解决此问题的算法吗?

题目解析

该题可以理解为字符串 t 中的每个字符都必须在字符串 s 截取的子字符串中出现,顺序不做要求,如下图所示:

image.png

从图中我们可以看到字符 A (蓝色边框)、字符 B (橙色边框)、字符 C (紫色边框)、其余字符(黑色边框)。

此时在字符串 s 中找到一段连续字符,包含字符串 t 中所有颜色的边框,所以此时符合条件的子字符串有: ADOBECBECODEBACODEBABANCADOBECODEBANC 等,你会发现所有字符串都包括字符 ABC ,且 数量大于等于 1

最终只需返回所有符合条件的子字符串中,长度最小的子字符串。

滑动窗口

首先,统计字符串 t 中,有几种不同的字符,每个字符数量为多少,可以将字符作为对象 need 的属性,属性值代表这个字符出现的次数,并且定义 length 存储字符串中存在多少种不同字符。

随后在右指针遍历字符串 s 的过程中,不断判断字符是否与字符串 t 中的某一个字符相同,如果相同,就将该字符作为属性存入对象 obj 中,该属性的初始值为 1 ,如果属性已存在,则该属性值加 1 。反之不做处理。

如果出现对象 need 和对象 obj 中相同属性的属性值相同,将满足条件数 sum 的值加 1 。直到出现 sum === length 时,右指针停止移动,并截取字符串 s[left, right - 1] 作为子字符串,左指针开始移动,移动过程中,如果对象 obj 的属性存在该字符,此时便判断对象 nedd 和对象 obj 中该属性的属性值是否相同,如果相同,满足条件数 sum 的值减 1 ,左指针停止移动,右指针继续移动。

明确左右指针的用处:

  • 左指针( left ):子字符串的左边界
  • 右指针( right ):子字符串的右边界

区间范围为 [left, right)

注意:在窗口滑动过程中,只能有一个指针滑动。

代码

/**
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
var minWindow = function(s, t) {
    let left = 0, right = 0, need = {}, obj = {}, sum = 0, length = 0, result = ""
    // 遍历字符串 t ,存储需要需要在字符串 s 中寻找的字符以及对应数量,存储字符串 t 的种类数
    for (const string of t) {
        if (need[string]) {
            need[string]++
        }else {
            need[string] = 1
            length++
        }
    }
    // 区间为左闭右开,所以采取小于判断条件
    while (right < s.length) {
        let c = s[right]
        // 判断字符 c 是否是字符串 t 需要的
        if (need[c]) {
            // 判断字符 c 在对象 obj 存储了没
            if (obj[c]) {
                obj[c]++
            }else {
                obj[c] = 1
            }
            if (need[c] === obj[c]) {
                sum++
            }
            right++
        }else {
            right++
        }
        // 此时左右指针切割下的子字符串包含字符串 t 中所有字符
        while (length === sum) {
            // 找到最短子字符串,并存储
            result = result.length > right - left || result === "" ? s.substr(left, right - left) : result
            if (obj[s[left]]) {
                if (obj[s[left]] === need[s[left]]) {
                    sum--
                }
                obj[s[left]]--
            }
            left++
        }
    }
    return result
};
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(1)O(1)

如图:

image.png

代码(优化)

主要从代码的简洁度和可读性的角度优化

/**
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
var minWindow = function(s, t) {
    let left = 0, right = 0, need = {}, obj = {}, sum = 0, length = 0, result = ""
    for (const string of t) {
        if (need[string]) {
            need[string]++
        }else {
            need[string] = 1
            length++
        }
    }
    
    while (right < s.length) {
        let c = s[right]
        if (need[c]) {
            // 优化
            obj[c] = obj[c] ? obj[c] + 1 : 1
            if (need[c] === obj[c]) {
                sum++
            }
        }
        right++
        while (length === sum) {
            result = result.length > right - left || result === "" ? s.substr(left, right - left) : result
            // 优化
            let start = s[left]
            if (obj[start]) {
                if (obj[start] === need[start]) {
                    sum--
                }
                obj[start]--
            }
            left++
        }
    }
    return result
};
  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(1)O(1)

如图:

image.png

执行用时和内存消耗仅供参考,大家可以多提交几次。如有更好的想法,欢迎大家提出。