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

一、前置知识(可跳过)

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 存储了字符的精确数量,确保窗口中字符的数量满足要求。

实例与展示

四、总结

再见!

相关推荐
kidding72312 分钟前
微信小程序怎么分包步骤(包括怎么主包跳转到分包)
前端·微信小程序·前端开发·分包·wx.navigateto·subpackages
微学AI27 分钟前
详细介绍:MCP(大模型上下文协议)的架构与组件,以及MCP的开发实践
前端·人工智能·深度学习·架构·llm·mcp
wuqingshun31415933 分钟前
蓝桥杯 10.拉马车
数据结构·c++·算法·职场和发展·蓝桥杯·深度优先
Java知识库37 分钟前
Java BIO、NIO、AIO、Netty面试题(已整理全套PDF版本)
java·开发语言·jvm·面试·程序员
liangshanbo12151 小时前
CSS 包含块
前端·css
Mitchell_C1 小时前
语义化 HTML (Semantic HTML)
前端·html
倒霉男孩1 小时前
CSS文本属性
前端·css
晚风3081 小时前
前端
前端
JiangJiang1 小时前
🚀 Vue 人如何玩转 React 自定义 Hook?从 Mixins 到 Hook 的华丽转身
前端·react.js·面试
鱼樱前端1 小时前
让人头痛的原型和原型链知识
前端·javascript