1 题目
给定两个字符串 s 和 t,长度分别是 m 和 n,返回 s 中的 最短窗口 子串 ,使得该子串包含 t 中的每一个字符(包括重复字符 )。如果没有这样的子串,返回空字符串""。
测试用例保证答案唯一。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
提示:
m == s.lengthn == t.length1 <= m, n <= 105s和t由英文字母组成
进阶: 你能设计一个在 O(m + n) 时间内解决此问题的算法吗?
2 代码实现
c++
cpp
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char,int> need ;
unordered_map<char,int> window;
for (char c :t ){
need[c]++;
}
int left = 0 , right = 0 ;
int valid = 0 ;
int start = 0 ;
int minLen = INT_MAX ;
while (right < s.size()){
char c = s[right];
right ++;
if (need.count(c)){
window[c]++;
if (window[c] == need[c]){
valid ++ ;
}
}
while(valid == need.size()){
if (right - left < minLen){
start = left ;
minLen = right - left ;
}
char d = s[left];
left ++ ;
if (need.count(d)){
if (window[d] == need[d]){
valid--;
}
window[d]--;
}
}
}
return minLen == INT_MAX ? "" : s.substr(start , minLen);
}
};
js
javascript
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
const need = new Map ();
const window = new Map();
for (const char of t ){
need.set(char , (need.get(char) || 0 ) + 1 );
}
let left = 0 ,right = 0 ;
let valid = 0 ;
let start = 0 ;
let minLen = Infinity ;
while (right < s.length){
const c = s[right];
right ++;
if (need.has(c)){
window.set(c , (window.get(c) || 0 ) + 1 );
if (window.get(c) === need.get(c)){
valid ++ ;
}
}
while(valid === need.size){
if (right - left < minLen ){
start = left ;
minLen = right - left ;
}
const d = s[left];
left ++;
if (need.has(d)){
if (window.get(d) === need.get(d)){
valid -- ;
}
window.set(d , window.get(d) - 1);
}
}
}
return minLen === Infinity ? "" : s.substring (start , start + minLen);
};
思考
啥啊,感觉好难,乱序的,hash map吗?
我现在看过了题解自己梳理一下啊。
看一下题目要求,s,t。t里面可能会有重复字符,先把t里面的有哈希表存储一遍,记一下需求,就比如a : 2 字母a需要2个,然后开始滑窗,如果没有达到t的需求,右指针移动。达到了,左指针收紧,维护一个minLen也就是最终我要返回字符串的位置。
题解
这道题标准解法是滑动窗口(双指针)+ 哈希表计数 ,时间复杂度 O(m + n),完美适配题目数据范围(1e5)。
核心思路(一定要看懂)
- 用哈希表 统计字符串
t中每个字符需要的数量(比如t=ABC,需要 A:1, B:1, C:1)。 - 用左右指针 维护一个滑动窗口
[left, right),右指针扩大窗口,左指针缩小窗口。 - 窗口内满足
t所有字符需求时,尽量缩小左指针,更新最小窗口。 - 最终返回最短的有效窗口,没有则返回空串。
完整 C++ 代码
cpp
#include <iostream>
#include <string>
#include <unordered_map>
#include <climits> // 用于 INT_MAX
using namespace std;
class Solution {
public:
string minWindow(string s, string t) {
// need:记录 t 中每个字符【需要的数量】
unordered_map<char, int> need;
// window:记录当前窗口内每个字符【已有的数量】
unordered_map<char, int> window;
// 第一步:统计 t 中所有字符的需求
for (char c : t) {
need[c]++;
}
int left = 0, right = 0; // 滑动窗口双指针 [left, right)
int valid = 0; // 窗口内【满足需求的字符种类数】
int start = 0; // 最小窗口起始下标
int minLen = INT_MAX; // 最小窗口长度(初始为无穷大)
// 右指针遍历整个 s,扩大窗口
while (right < s.size()) {
char c = s[right]; // 即将加入窗口的字符
right++; // 右指针右移
// ② 如果这个字符是 t 里需要的,更新窗口计数
if (need.count(c)) {
window[c]++;
// 当窗口内该字符数量 = 需求数量,valid+1
if (window[c] == need[c]) {
valid++;
}
}
// ③ 当窗口满足所有字符需求时,开始【收缩左指针】,找更小窗口
while (valid == need.size()) {
// ✅ 更新最小窗口
if (right - left < minLen) {
start = left;
minLen = right - left;
}
char d = s[left]; // 即将移出窗口的字符
left++; // 左指针右移
// ④ 如果移出的是需要的字符,判断是否破坏 valid
if (need.count(d)) {
if (window[d] == need[d]) {
valid--;
}
window[d]--;
}
}
}
// ⑤ 没有找到有效窗口返回空串,否则返回截取的子串
return minLen == INT_MAX ? "" : s.substr(start, minLen);
}
};
// 测试代码
int main() {
Solution sol;
// 示例 1
string s1 = "ADOBECODEBANC", t1 = "ABC";
cout << sol.minWindow(s1, t1) << endl; // 输出 BANC
// 示例 2
string s2 = "a", t2 = "a";
cout << sol.minWindow(s2, t2) << endl; // 输出 a
// 示例 3
string s3 = "a", t3 = "aa";
cout << sol.minWindow(s3, t3) << endl; // 输出空串
return 0;
}
逐行关键逻辑讲解
1. 两个哈希表的作用
need:存t里每个字符需要多少个 例:t = "ABC"→need: A→1, B→1, C→1window:存当前窗口里每个字符有多少个
2. 滑动指针规则
- 右指针
right:只管扩大窗口,把字符加进来 - 左指针
left:只有窗口满足条件 时,才缩小窗口,尝试找更短的子串
3. 核心判断:valid
valid表示窗口内已经满足数量要求的字符种类数- 当
valid == need.size()→ 窗口完全包含 t,可以开始缩左指针
4. 最小窗口更新
cpp
if (right - left < minLen) {
start = left;
minLen = right - left;
}
每次缩窗口时,都检查当前窗口是不是更短,如果是就记录新的起点和长度。
5. 结果返回
minLen == INT_MAX→ 没找到有效窗口,返回""- 否则返回
s.substr(start, minLen)(C++ 截取子串函数)
复杂度分析
- 时间复杂度 :
O(m + n)左右指针各遍历字符串一次,哈希表操作是O(1) - 空间复杂度 :
O(k),k是字符集大小(最多 52 个大小写字母)
3 题目
困难
相关标签

相关企业
给定一个字符串 s和一个字符串数组 words。 words 中所有字符串 长度相同。
s中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。
- 例如,如果
words = ["ab","cd","ef"], 那么"abcdef","abefcd","cdabef","cdefab","efabcd", 和"efcdab"都是串联子串。"acdbef"不是串联子串,因为他不是任何words排列的连接。
返回所有串联子串在 s中的开始索引。你可以以 任意顺序 返回答案。
示例 1:
输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。
子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。
子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。
输出顺序无关紧要。返回 [9,0] 也是可以的。
示例 2:
输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出:[]
解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。
s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。
所以我们返回一个空数组。
示例 3:
输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出:[6,9,12]
解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。
子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。
子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。
子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。
提示:
1 <= s.length <= 1041 <= words.length <= 50001 <= words[i].length <= 30words[i]和s由小写英文字母组成
4 代码实现
c++
cpp
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
vector <int> res ;
if (s.empty() || words.empty()) return res ;
int wordLen = words[0].size();
int wordCnt = words.size();
int totalLen = wordLen * wordCnt ;
int sLen = s.size();
unordered_map<string , int > need ;
for (string& word : words){
need[word] ++;
}
for(int i = 0 ; i< wordLen ; i++){
int left = i ;
int valid = 0 ;
unordered_map<string , int > window ;
for (int right = i ; right + wordLen <= sLen ; right += wordLen){
string curWord = s.substr(right , wordLen);
if(need.count(curWord)){
window[curWord] ++;
if (window[curWord] == need[curWord]){
valid ++ ;
}
}
while (right - left + wordLen > totalLen){
string leftWord = s.substr(left , wordLen);
left += wordLen ;
if (need.count(leftWord)){
if(window[leftWord] == need[leftWord]){
valid -- ;
}
window[leftWord] -- ;
}
}
if(valid == need.size()){
res.push_back(left);
}
}
}
return res ;
}
};
js
javascript
function findSubstring(s, words) {
const res = [];
if (!s || words.length === 0) return res;
const wordLen = words[0].length; // 每个单词长度
const wordCount = words.length; // 单词总数
const totalLen = wordLen * wordCount; // 目标子串必须这么长
const sLen = s.length;
// 统计 words 里每个单词需要出现几次
const need = {};
for (const w of words) {
need[w] = (need[w] || 0) + 1;
}
// 关键:按单词长度分组,每组起始点 i = 0,1,...,wordLen-1
for (let i = 0; i < wordLen; i++) {
let left = i;
let valid = 0;
const window = {}; // 当前窗口内单词计数
// 右指针每次跳一个单词长度
for (let right = i; right + wordLen <= sLen; right += wordLen) {
// 取出当前单词
const cur = s.slice(right, right + wordLen);
// 如果这个单词是需要的
if (need[cur] !== undefined) {
window[cur] = (window[cur] || 0) + 1;
if (window[cur] === need[cur]) {
valid++; // 匹配成功一种单词
}
}
// 窗口太长了,收缩左边
while (right - left + wordLen > totalLen) {
const leftWord = s.slice(left, left + wordLen);
left += wordLen;
if (need[leftWord] !== undefined) {
if (window[leftWord] === need[leftWord]) {
valid--;
}
window[leftWord]--;
}
}
// 所有单词都匹配上了,记录起点
if (valid === Object.keys(need).length) {
res.push(left);
}
}
}
return res;
}
思考
这啥意思,不懂啊,之前都是可以直接统计出来的,但是这里需要啊,需要内部是有顺序的,因为原先的字符不能打乱,这咋办啊??
我看了题解,写一写我自己的理解啊,这个题目比较重要的特点是把单词作为移动依据,也就是boy 是可以的,oyb是不ok的。那么还有一个要点就是,每一个单词的长度都是一样的,man , boy , eat ,长度都是一样的。
做法是,取string,长度已知,要移动一端另一端就也了解了,写不下去了,bbbboyman这样有杂乱字符的呢,其实也是i = 0 , i = 1 ,一个一个字符走。在 s 里找一段长度 = totalLen 的子串,恰好包含 words 里所有单词(数量、种类都一样)要是监测到byo会怎么样?
但是整体找子串的时候,滑窗是按照单词长度走的。
先回答一下以上加粗的疑问:
- 字符串 s 是连续的,不能打乱! 比如
bbbboyman里,必须按顺序切分 :bboyba... 不能乱拼 - 为什么不能一个字符一个字符滑动? 可以滑,但会超时!而且切分单词会乱掉!
- **如果滑到 byo、oym 这种不是单词的东西怎么办?**直接判定无效,跳过!
- 为什么要分 i=0、i=1、i=2 三组? 因为单词是固定长度 ,必须按固定间隔切分才是合法单词!
这道题 = 按固定长度切块 + 滑动窗口
单词长度 = 3那么字符串 s 只能切成下面 3 种切法 ,没有第 4 种!
cpp
切法1(i=0):0-2, 3-5, 6-8...
切法2(i=1):1-3, 4-6, 7-9...
切法3(i=2):2-4, 5-7, 8-10...
任何其他切法,切出来的都不是单词!直接无效!
我拿你说的 bbbarfoo 现场演示(一看就懂)
s = b b b a r f o o words = ["bar","foo"]单词长度 = 3
只能切成 3 种方式:
① 第 1 组:i=0(从第 0 个字符开始切)
切出来的块是:[bbb] [arr] [foo]→ bbb 不是单词 → 无效→ arr 不是单词 → 无效→ 整段都废了 ❌
② 第 2 组:i=1(从第 1 个字符开始切)
块:bba rfo......全都不是单词 ❌
③ 第 3 组:i=2(从第 2 个字符开始切)
块:bbarfoo切法:[b b a] → 无效[r f o] → 无效❌
真正正确的那个位置在哪里?
s = b b b a r f o o 正确开始位置是 3
它属于 i=0 组 切出来是:[bar] [foo]✅ 正好是两个单词!匹配成功!
如果切到 byo、oym、asd 这种不是单词的块,怎么办?
直接判定这个窗口无效,跳过,继续往后滑!
代码里就是这句:
cpp
if (need.count(curWord)) {
// 是单词才加入统计
}
不是单词?直接不理它!
你自己的理解已经90% 正确了,我帮你理顺:
- 所有单词长度一样 → 必须按这个长度切块
- s 不能打乱 → 只能按固定间隔切分成单词
- 一共只有 单词长度种切法
- 对每一种切法,滑动窗口检查是否正好包含所有单词
- 遇到不是单词的块 → 直接跳过
固定长度切单词,只分几组不混乱。 是单词就进窗口, 不是单词直接扔。 凑齐所有单词时, 记录起点就满分!
题解
核心思路(超级好懂)
- words 里所有单词长度相同 → 设为
wordLen - 串联子串总长度固定 :
totalLen = 单词个数 × 每个单词长度 - 本质 :在
s里找一段长度 =totalLen的子串,恰好包含 words 里所有单词(数量、种类都一样) - 滑动窗口 :按单词块移动窗口,用哈希表计数匹配
完整 C++ 代码
cpp
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
using namespace std;
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
vector<int> res; // 存答案索引
if (s.empty() || words.empty()) return res;
int wordLen = words[0].size(); // 每个单词的长度(固定)
int wordCnt = words.size(); // 单词总个数
int totalLen = wordLen * wordCnt; // 串联子串的总长度
int sLen = s.size();
// 哈希表1:记录 words 里每个单词需要的数量
unordered_map<string, int> need;
for (string& word : words) {
need[word]++;
}
// 技巧:窗口起点只能是 0、1、2 ... wordLen-1,否则会重复遍历
for (int i = 0; i < wordLen; i++) {
int left = i; // 窗口左边界
int valid = 0; // 匹配成功的单词数量
unordered_map<string, int> window; // 哈希表2:当前窗口内的单词计数
// 右边界按【单词长度】移动
for (int right = i; right + wordLen <= sLen; right += wordLen) {
// 取出当前单词
string curWord = s.substr(right, wordLen);
// 情况1:这个单词在需求里 → 加入窗口
if (need.count(curWord)) {
window[curWord]++;
// 数量匹配 → valid+1
if (window[curWord] == need[curWord]) {
valid++;
}
}
// 当窗口长度超过总长度 → 收缩左边界
while (right - left + wordLen > totalLen) {
// 左边要移出的单词
string leftWord = s.substr(left, wordLen);
left += wordLen; // 左指针移动一个单词
// 如果移出的是需要的单词
if (need.count(leftWord)) {
if (window[leftWord] == need[leftWord]) {
valid--;
}
window[leftWord]--;
}
}
// 所有单词都匹配成功 → 记录起点 left
if (valid == need.size()) {
res.push_back(left);
}
}
}
return res;
}
};
// 测试代码
int main() {
Solution sol;
string s1 = "barfoothefoobarman";
vector<string> w1 = {"foo","bar"};
vector<int> ans1 = sol.findSubstring(s1, w1);
for(int x : ans1) cout << x << " "; // 0 9
return 0;
}
逐行精讲
1. 关键变量
wordLen:每个单词长度(固定)wordCnt:words 里有多少个单词totalLen:答案子串必须是这个长度need:记录 words 里每个单词需要多少个window:记录当前窗口里有多少个单词valid:已经匹配成功的单词种类数
2. 最关键的外层循环
cpp
for (int i = 0; i < wordLen; i++)
- 因为单词是固定长度 ,窗口起点只能是
0、1、2 ... wordLen-1 - 这样不会重复遍历,保证效率最高
3. 滑动窗口逻辑
- 右指针 :每次移动一个单词长度
- 加入当前单词到哈希表
- 窗口超长 时,左指针移动一个单词长度
- 匹配成功 :
valid == need.size()→ 记录左指针位置
4. 匹配成功条件
- 窗口里每个单词的数量和 words 完全一样
- 此时
valid等于需要的单词种类数 - 把窗口左起点加入答案
复杂度分析(最优解)
- 时间复杂度 :O(s.length × wordLen)
- 空间复杂度:O (words 总字符数)
- 完全能过题目所有数据范围(1e4)
测试示例
cpp
s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出:[]
s = "barfoofoobarthefoobarman", words = ["bar","foo","the"]
输出:[6,9,12]
总结
这道题 = 最小覆盖子串的升级版
- 上一题按字符滑动
- 这一题按单词块滑动
- 哈希表计数逻辑完全一样
5 小结
这两题我觉得我什么都不会。我一直在抄答案看题解。
最小覆盖子串 按 字符 滑动
串联所有单词的子串 按 单词块 滑动
模板
1. 统计需求 need
2. 初始化 left, right, valid
3. 右指针一直往右走
4. 符合需求就更新 window 和 valid
5. 当 valid 满足 → 收缩左指针
6. 记录答案