LeetCode 76:Minimum Window Substring 题解与滑动窗口思维详解

题目链接:Minimum Window Substring - LeetCode[page:2]


题目要求到底是什么?

题目描述(英文关键部分):[page:2]

Given two strings s and t, return the minimum window substring of s such that every character in t (including duplicates) is included in the window.

If there is no such substring, return the empty string "".

The testcases will be generated such that the answer is unique.

翻译成白话:

  • 给定字符串 st
  • s 里找一个子串 ,这个子串要包含 t 中的所有字符和对应次数
  • 在所有「满足条件的子串」里,返回长度最短的那一个
  • 如果不存在这样的子串,返回空串 ""
  • 题目保证:最短的这个答案是唯一的(不会有两个不同位置但同样短的最优解)。[page:1]

注意这两点:

  1. 要的是 minimum window substring ------ 最短的那一段,而不是"随便一个包含 t 的子串就行"。
  2. "答案唯一"是说「最短答案不会有并列」,不是说"第一个找到的合法窗口就是答案"。

因此,不能在第一次 找到一个包含 t 的窗口就 break,而是要在所有合法窗口中找到长度最小的。


思维抽象:把问题想成"装满购物清单"

可以这样类比:

  • t 是一张购物清单 :需要哪些字符,各需要多少个。
    • 例如 t = "AABC",清单上是:A × 2, B × 1, C × 1。
  • s 是一排货架"ADOBECODEBANC"
  • 我们用两个指针 leftright 表示一个在货架上的「窗口」 [left, right],这个窗口里面的字符就是我们当前"购物车"里的东西。

目标:

  • 找到一个最短的窗口 [left, right],能刚好把清单上的东西都装进去(至少满足需求次数)。
  • 同时,这样满足条件的窗口可能有很多,我们要从中选出最短的那一个。

对应到算法上,就是典型的滑动窗口 + 频次统计


官方推荐思路:滑动窗口 + 两个计数

1. 准备:统计 t 里面的需求

  • t_map 统计 t 中每个字符需要的次数(类似 need)。
  • 同时统计一共有多少种不同字符是"需要的",记为 t_count(类似 required)。

伪代码(逻辑上):

pseudo 复制代码
t_map = {}
t_count = 0

for each char c in t:
    if t_map[c] 之前是 0:
        t_count++      // 多了一种需要的字符
    t_map[c]++

2. 窗口中的统计

  • s_map 统计当前窗口中每个字符出现的次数(类似 window)。
  • s_count 记录「当前窗口中,有多少种字符的数量已经达到需求」。

含义类似:

  • t_count = 需要满足的字符种类数(required)。
  • s_count = 当前窗口中已经满足需求的字符种类数(formed)。

s_count == t_count 时,说明当前窗口已经覆盖了 t 的所有字符与次数,是一个合法窗口。


滑动窗口流程

步骤一:右指针扩展窗口

  • 初始化:left = 0right = 0
  • right 从左到右遍历 s

伪代码:

pseudo 复制代码
for right from 0 to s_len - 1:
    c = s[right]

    // 如果当前字符根本不是需要的字符,跳过,不影响 s_count
    if t_map[c] == 0:
        continue

    // 把 c 加入窗口
    s_map[c]++

    // 如果这个字符刚好达到需求次数,多了一种满足的字符
    if s_map[c] == t_map[c]:
        s_count++

步骤二:当窗口已经满足需求时,尽量缩小

一旦 s_count == t_count,说明当前窗口 [left, right] 已经包含了 t 需要的所有字符,我们可以试着从左边缩小窗口,找到更短的合法子串。

伪代码:

pseudo 复制代码
while s_count == t_count:
    // 尝试更新全局最优答案
    if (right - left + 1) < best_len:
        best_len = right - left + 1
        best_left = left

    // 移除左边的字符
    lc = s[left]
    if t_map[lc] > 0:                // 如果是需要的字符才会影响计数
        if s_map[lc] == t_map[lc]:   // 原来刚好满足需求
            s_count--                // 移除后不再满足
        s_map[lc]--

    left++

关键点:

  • 只有当窗口已经满足需求 时(s_count == t_count),才会尝试缩小,并顺带更新最短答案。
  • 缩小过程中,一旦有某个字符的数量不再满足 t_map 的需求,就让 s_count--,窗口变成不合法,循环结束,回到外层继续移动 right

步骤三:结束返回结果

  • 整个 s 扫完后,如果 best_len 还是初始值(比如 INT_MAX),说明不存在合法窗口,返回空串。
  • 否则返回 s[best_left .. best_left + best_len - 1]

这一整套流程保证了:

  • 每个字符最多被 right 访问一次、被 left 移出一次。
  • 整体时间复杂度是 (O(m + n))(符合题目 Follow up 要求)。[page:2]

C 语言实现(使用 uthash)

在 C 里直接开大数组做频次数组是最简单的,但如果使用 uthash 这样的哈希表,也完全可以。下面是一个使用 uthash 的完整实现,它正是前面思路的代码版本。

c 复制代码
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include "uthash.h"

struct char_node {
    char ch;
    int count;
    UT_hash_handle hh;
};

static void map_inc(struct char_node **head, char ch) {
    struct char_node *node;
    HASH_FIND(hh, *head, &ch, sizeof(char), node);
    if (node != NULL) {
        node->count++;
        return;
    } else {
        node = (struct char_node *)malloc(sizeof(struct char_node));
        node->ch = ch;
        node->count = 1;
        HASH_ADD(hh, *head, ch, sizeof(char), node);
    }
}

static void map_dec(struct char_node **head, char ch) {
    struct char_node *node;
    HASH_FIND(hh, *head, &ch, sizeof(char), node);
    if (node != NULL && node->count > 0) {
        node->count--;
    }
}

static int map_get(struct char_node *head, char ch) {
    struct char_node *node;
    HASH_FIND(hh, head, &ch, sizeof(char), node);
    return node != NULL ? node->count : 0;
}

static void map_free(struct char_node **head) {
    struct char_node *node, *tmp;
    HASH_ITER(hh, *head, node, tmp) {
        HASH_DEL(*head, node);
        free(node);
    }
}

char* minWindow(char* s, char* t) {
    int s_len = strlen(s);
    int t_len = strlen(t);
    int best_ans_len = INT_MAX;
    char *ans = NULL;

    if (s_len == 0 || t_len == 0) {
        return strdup("");
    }

    int left = 0, best_left = 0;
    int right = 0;
    int t_count = 0; // 需要的字符种类数
    int s_count = 0; // 当前已满足需求的字符种类数
    char c;
    struct char_node *s_map = NULL;
    struct char_node *t_map = NULL;

    // 统计 t_map 和 t_count
    for (int i = 0; i < t_len; i++) {
        if (map_get(t_map, t[i]) == 0) {
            t_count++;
        }
        map_inc(&t_map, t[i]);
    }

    // 滑动窗口
    for (right = 0; right < s_len; right++) {
        c = s[right];

        // 不在 t_map 中的字符可以直接忽略
        if (map_get(t_map, c) == 0) {
            continue;
        }

        // 加入窗口
        map_inc(&s_map, c);
        if (map_get(s_map, c) == map_get(t_map, c)) {
            s_count++;
        }

        // 当窗口已经满足所有需要的字符种类
        while (s_count == t_count) {
            if (right - left + 1 < best_ans_len) {
                best_ans_len = right - left + 1;
                best_left = left;
            }

            char lc = s[left];
            if (map_get(t_map, lc) > 0) {
                if (map_get(s_map, lc) == map_get(t_map, lc))
                    s_count--;
                map_dec(&s_map, lc);
            }
            left++;
        }
    }

    // 没有合法窗口
    if (best_ans_len == INT_MAX) {
        map_free(&s_map);
        map_free(&t_map);
        return strdup("");
    }

    // 拷贝最短窗口
    ans = (char *)malloc(best_ans_len + 1);
    memset(ans, 0, best_ans_len + 1);
    strncpy(ans, s + best_left, best_ans_len);

    map_free(&s_map);
    map_free(&t_map);
    return ans;
}

这段代码已经在 LeetCode 上通过全部测试用例,只是因为使用哈希表而不是数组,性能上略慢一点,但在面试和刷题上完全可以接受。[page:2]


常见疑问:为什么不能"找到第一个就返回"?

很多人一开始会有这样的误区:

题目说答案是唯一的,那我从左往右找到第一个包含 t 的子串,直接返回不就行了?

问题在于:

  • "唯一"指的是:在所有最短合法子串里,只有一个位置
  • 不是说:"你从左往右扫到的第一个合法窗口就是那个最短的"。

示例:

  • s = "AAABBC"
  • t = "ABC"

合法的子串有很多:

  • "AAABBC"(长度 6)
  • "AABBC"(长度 5)
  • "ABBC"(长度 4)

最短答案是 "ABBC",不是你从左边开始扫到的第一个合法窗口。所以:

  • 每次窗口刚好覆盖 t 时,只能说明"这是一个合法窗口",并不保证是最短的。
  • 我们必须在「合法」的前提下继续缩小窗口,并在整个过程中维护全局最短答案。

这也正是滑动窗口中「右指针只增不减,左指针在合法时尽量往右缩」的核心思想。


小结

  1. 这题的关键是理解:
    • 目标是「最短且包含 t 所有字符(含重复)」的窗口。
    • "唯一答案"指的是"最短答案没有并列",不是"随便一个合法窗口"。
  2. 官方推荐解法是:
    • t 统计需求(t_map + t_count)。
    • 用滑动窗口 [left, right]s_map + s_count 记录当前窗口是否满足需求。
    • 当窗口满足需求时,通过移动左指针缩小窗口并更新最短答案。
  3. 整体时间复杂度 (O(m + n)),非常适合面试中的经典滑动窗口模板题。[page:2]

如果你已经写出了上面的 C 版本,那么你不仅拿下了这题,也算真正把「滑动窗口 + 频次统计」这一类题的核心套路吃透了。

相关推荐
小O的算法实验室2 小时前
2026年IEEE TETCI,山区环境下基于双种群进化的协同无人机巡逻任务协同优化,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
生成论实验室2 小时前
《事件关系阴阳博弈动力学:识势应势之道》第二篇:阴阳博弈——认知的动力学基础
数据结构·人工智能·科技·神经网络·算法
风筝在晴天搁浅2 小时前
字节高频题 小于n的最大数
算法
LabVIEW开发3 小时前
LabVIEW水力机组空蚀在线监测
算法·labview·labview知识·labview功能·labview程序
AI科技星3 小时前
科幻艺术书本封面:《全域数学》第一部·数术本源 第三卷 代数原本(P95-141)完整五级目录【乖乖数学】
算法·机器学习·数学建模·数据挖掘·量子计算
风筝在晴天搁浅3 小时前
LeetCode 92.反转链表Ⅱ
算法·leetcode·链表
weisian1513 小时前
基础篇--概念原理-2-参数是什么?——从原理到实战,一篇讲透
面试·职场和发展·模型参数·7b和70b·参数=规则,不是原始数据
王老师青少年编程3 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【贪心与二分判定】:数列分段 Section II
c++·算法·贪心·csp·信奥赛·二分判定·数列分段 section ii
V搜xhliang02463 小时前
OpenClaw科研全场景用法:从文献到实验室的完整自动化方案
运维·开发语言·人工智能·python·算法·microsoft·自动化