一分钟解决 | 高频面试算法题——最小覆盖子串

一、前置知识(可跳过)

1. 什么是 Map?

Map 对象是一种键值对集合,类似于 JavaScript 中的对象(Object),但 Map 和对象有一些重要的区别:

  • 键的类型: Map 的键可以是任意数据类型(包括对象、函数、以及其他原始类型),而对象(Object)的键只能是字符串或 Symbol。
  • 键的顺序: Map 会维护键的插入顺序,也就是说,当你使用 Map 遍历元素时,元素的顺序与你插入的顺序相同。 对象不保证键的顺序。
  • 原型继承 Map 没有原型,这意味着你不会意外地从原型链上获取到不属于 Map 自身的数据。对象会从原型链上继承一些属性和方法,可能会导致意外行为。
  • 易于获取大小 可以使用size属性轻松获取Map的大小,不用像对象一样需要手动计算。
  • 性能: 在频繁添加和删除键值对的场景中,Map 通常比对象有更好的性能。

2. 创建 Map 对象

javascript 复制代码
// 创建一个空 Map
const myMap = new Map();

// 创建一个包含初始键值对的 Map (二维数组)
const myMapWithValues = new Map([
    ['key1', 'value1'],
    ['key2', 'value2'],
    [123, 'number_key']
]);

3. Map 的常用方法

  • set(key, value): 添加或更新键值对

    javascript 复制代码
    const myMap = new Map();
    myMap.set('name', 'Alice');
    myMap.set('age', 30);
    myMap.set('name', 'Bob'); // 更新 'name' 的值
    
    console.log(myMap); // Map(2) { 'name' => 'Bob', 'age' => 30 }
  • get(key): 获取键对应的值

    javascript 复制代码
    const myMap = new Map([['name', 'Charlie'], ['city', 'London']]);
    console.log(myMap.get('name'));   // 输出: Charlie
    console.log(myMap.get('city'));   // 输出: London
    console.log(myMap.get('country')); // 输出: undefined (键不存在)
  • has(key): 检查是否包含指定键

    javascript 复制代码
    const myMap = new Map([['a', 1], ['b', 2]]);
    console.log(myMap.has('a')); // 输出: true
    console.log(myMap.has('c')); // 输出: false
  • delete(key): 删除指定键值对

    javascript 复制代码
    const myMap = new Map([['x', 10], ['y', 20]]);
    console.log(myMap.delete('x')); // 输出: true (删除成功)
    console.log(myMap.delete('z')); // 输出: false (键不存在)
    console.log(myMap);             // Map(1) { 'y' => 20 }
  • clear(): 清空 Map

    javascript 复制代码
    const myMap = new Map([['p', 100], ['q', 200]]);
    myMap.clear();
    console.log(myMap); // Map(0) {}
  • size: 获取 Map 的大小(键值对的数量)

    javascript 复制代码
    const myMap = new Map([['apple', 1], ['banana', 2], ['cherry', 3]]);
    console.log(myMap.size); // 输出: 3

4. 遍历 Map

Map 提供了多种遍历方式:

  • keys(): 获取所有键的迭代器

    javascript 复制代码
    const myMap = new Map([['a', 1], ['b', 2], ['c', 3]]);
    for (const key of myMap.keys()) {
        console.log(key); // 输出: a, b, c
    }
  • values(): 获取所有值的迭代器

    javascript 复制代码
    const myMap = new Map([['a', 1], ['b', 2], ['c', 3]]);
    for (const value of myMap.values()) {
        console.log(value); // 输出: 1, 2, 3
    }
  • entries(): 获取所有键值对的迭代器 (返回 [key, value] 数组)

    javascript 复制代码
    const myMap = new Map([['a', 1], ['b', 2], ['c', 3]]);
    for (const [key, value] of myMap.entries()) {
        console.log(key, value); // 输出: a 1, b 2, c 3
    }
  • forEach(callbackFn, thisArg): 类似于数组的 forEach

    javascript 复制代码
    const myMap = new Map([['a', 1], ['b', 2], ['c', 3]]);
    myMap.forEach((value, key) => {
        console.log(key, value); // 输出: a 1, b 2, c 3
    });

5. Map 的应用场景

  • 缓存: 可以利用 Map 存储计算结果,避免重复计算。
  • 键值对存储: 当键的类型不确定时(例如对象),Map 比对象更合适。
  • 需要维护插入顺序的场景: Map 保证键的插入顺序,这在某些场景下非常重要。
  • 统计词频: 使用 Map 可以方便地统计每个词出现的次数。
  • 存储元素相关的元数据: 可以存储与 DOM 元素或其他对象相关的额外信息。

6. Map 与 Object 的选择

  • 如果键始终是字符串或 Symbol,并且不需要键的顺序,对象可能是更简单的选择。
  • 如果键的类型不确定,或者需要维护键的顺序,或者需要频繁添加和删除键值对,Map 通常是更好的选择。
  • 总的来说, Map 是更为强大的数据结构。

总结

Map 是 JavaScript 中一种非常有用的数据结构,它提供了比对象更灵活和强大的键值对存储能力。 掌握 Map 的使用方法,可以使你的代码更加简洁、高效。

二、题目描述

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

注意:

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

示例 1:

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

示例 2:

ini 复制代码
输入: s = "a", t = "a"
输出: "a"
解释: 整个字符串 s 是最小覆盖子串。

示例 3:

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

提示:

  • m == s.length
  • n == t.length
  • 1 <= m, n <= 105
  • st 由英文字母组成

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

三、题解(滑动窗口)

js 复制代码
/**
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
var minWindow = function(s, t) {
    if (!s || !t || s.length === 0 || t.length === 0) {
        return "";
    }

    const tMap = new Map(); // 存储 t 中每个字符出现的次数
    for (const char of t) {
        tMap.set(char, (tMap.get(char) || 0) + 1);
    }

    let left = 0; // 滑动窗口的左指针
    let right = 0; // 滑动窗口的右指针
    let minLen = Infinity; // 最小子串的长度,初始化为无穷大
    let start = 0; // 最小子串的起始位置

    let matched = 0; // 记录窗口中满足 t 字符数量的字符个数

    const windowMap = new Map(); // 存储滑动窗口中每个字符出现的次数

    while (right < s.length) {
        const char1 = s[right]; // 获取右指针指向的字符
        if (tMap.has(char1)) {
            // 如果该字符是 t 中需要的字符
            windowMap.set(char1, (windowMap.get(char1) || 0) + 1); // 更新窗口中该字符的数量
            if (windowMap.get(char1) === tMap.get(char1)) {
                // 如果窗口中该字符的数量已经满足 t 中该字符的数量,则 matched 加 1
                matched++;
            }
        }
        right++; // 右指针右移

        // 当窗口中所有 t 中的字符都满足数量时,尝试缩小窗口
        while (matched === tMap.size) {
            // 更新最小子串
            if (right - left < minLen) {
                minLen = right - left;
                start = left;
            }

            const char2 = s[left]; // 获取左指针指向的字符
            if (tMap.has(char2)) {
                // 如果该字符是 t 中需要的字符
                windowMap.set(char2, windowMap.get(char2) - 1); // 窗口中该字符的数量减 1
                if (windowMap.get(char2) < tMap.get(char2)) {
                    // 如果窗口中该字符的数量小于 t 中该字符的数量,则 matched 减 1
                    matched--;
                }
            }
            left++; // 左指针右移,缩小窗口
        }
    }

    // 如果 minLen 没有被更新,则说明没有找到符合条件的子串
    return minLen === Infinity ? "" : s.substring(start, start + minLen);
};

核心思想

这个算法采用的是滑动窗口的思想,它通过维护一个动态大小的窗口 ,在源字符串 s 上滑动,寻找满足特定条件的子串。 在本题中,这个特定条件是窗口需要包含目标字符串 t 中所有字符,并且字符的数量要不少于 t 中对应字符的数量。算法的目标是找到满足这个条件的最小窗口。

详细步骤

  1. 初始化:

    • 进行空值检查:如果 st 为空,直接返回空字符串 ""
    • 创建 tMap:使用 Map 存储字符串 t 中每个字符出现的次数。 这作为寻找覆盖子串的参考。
    • 初始化窗口指针:设置左指针 left 为 0,右指针 right 为 0。
    • 初始化结果变量:设置 minLenInfinity(无穷大,用于跟踪最小长度),start 为 0(用于记录最小子串的起始位置)。
    • 初始化匹配计数器:设置 matched 为 0,用于记录窗口中满足 t 字符数量要求的字符个数。
    • 创建 windowMap:使用 Map 存储滑动窗口中每个字符出现的次数。
  2. 扩展窗口 (移动右指针):

    • while (right < s.length) 循环:不断移动右指针 right 遍历字符串 s

    • 获取当前字符:使用 char1 = s[right] 获取 right 指针指向的字符。

    • 检查字符是否在 tMap 中:if (tMap.has(char1)) 判断字符是否是目标字符串 t 中需要包含的字符。

      • 更新 windowMap:如果字符是需要的,更新 windowMap 中该字符的计数。
      • 更新 matched:如果 windowMap 中该字符的计数等于 tMap 中该字符的计数,说明窗口中该字符的数量已经满足要求,matched++
    • 右指针右移:right++,扩大窗口。

  3. 收缩窗口 (移动左指针):

    • while (matched === tMap.size) 循环:当 matched 等于 tMap.size 时,说明当前窗口已经包含了 t 中所有字符,并且数量满足要求。 现在尝试缩小窗口,寻找更小的覆盖子串。

    • 更新最小子串信息:如果当前窗口的长度 (right - left) 小于 minLen,则更新 minLenstart,记录更小的覆盖子串。

    • 获取左指针字符:使用 char2 = s[left] 获取 left 指针指向的字符。

    • 检查字符是否在 tMap 中:if (tMap.has(char2))

      • 更新 windowMap: 如果该字符是需要的,将 windowMap中该字符的数量减 1。
      • 更新 matched:如果窗口中该字符的数量小于 tMap 中该字符的数量,则将 matched 减 1,表示不再满足覆盖要求。
    • 左指针右移:left++,缩小窗口。

  4. 返回结果:

    • 如果 minLen 仍然是 Infinity,说明没有找到满足条件的子串,返回空字符串 ""
    • 否则,使用 s.substring(start, start + minLen) 提取最小子串并返回。

特别注意

  1. Map 的使用: 正确使用 Maphasgetset 方法是关键。 Map 使得字符计数和查找变得非常高效。
  2. matched 计数器的维护: matched 计数器的正确维护是算法的核心。 它准确地记录了窗口中已满足要求的字符种类数量。 matched 的增加和减少必须与 windowMaptMap 的状态同步。
  3. 窗口收缩的条件: 只有当 matched === tMap.size 时才能收缩窗口。 否则,窗口需要继续扩展,直到覆盖所有需要的字符。
  4. 更新最小子串的时机: 只有在 matched === tMap.size 的情况下,才需要检查并更新 minLenstart
  5. 边界条件处理: 注意处理空字符串的情况,以及没有找到满足条件子串的情况。
  6. 重复字符的处理: 算法能够正确处理 t 中包含重复字符的情况。 windowMaptMap 存储了字符的精确数量,确保窗口中字符的数量满足要求。

实例与展示

四、总结

再见!

相关推荐
Epiphany.55610 分钟前
蓝桥杯备赛题目-----爆破
算法·职场和发展·蓝桥杯
Ticnix12 分钟前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人15 分钟前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl19 分钟前
OpenClaw 深度技术解析
前端
崔庆才丨静觅23 分钟前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人31 分钟前
vue3使用jsx语法详解
前端·vue.js
YuTaoShao33 分钟前
【LeetCode 每日一题】1653. 使字符串平衡的最少删除次数——(解法三)DP 空间优化
算法·leetcode·职场和发展
天蓝色的鱼鱼34 分钟前
shadcn/ui,给你一个真正可控的UI组件库
前端
茉莉玫瑰花茶38 分钟前
C++ 17 详细特性解析(5)
开发语言·c++·算法
布列瑟农的星空38 分钟前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust