送给大家一句话:
充满着欢乐与斗争精神的人们,永远带着欢乐,欢迎雷霆与阳光。 —— 赫胥黎
滑动窗口精通
- 前言
- Leetcode 30. 串联所有单词的子串
- 题目描述
- 算法思路
- Leetcode 76. 最小覆盖子串
- 题目描述
- 算法思路
- Thanks♪(・ω・)ノ 谢谢阅读!!!
- 下一篇文章见!!!
前言
相信通过前两篇的文章的讲解,大家已经对滑动窗口有了较深的认识,今天我们来挑战一下!!!
来做两道困难级的题目。
Leetcode 30. 串联所有单词的子串
家人们!!!上链接!!!30. 串联所有单词的子串
题目描述
根据题目描述,这道题看起来很是复杂呢!?!?
我们要在一个字符串中寻找包含words的所有单词的子串,并会返回对应的开始位置(开始索引)。看这描述十分类似这道题目 438. 找到字符串中所有字母异位词 ,一个是寻找异位词,一个是寻找"异位单词串"。本质是十分相似的。
来看一个样例:
- 输入: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] 也是可以的。
根据这样例,更容易想象为是如同字母一样。
算法思路
我们先把每个单词抽象为一个字母(方便我们梳理思路),我们只需要找到一个子串中有所有的“字母”即可。
那么,我们来看最简单的算法:暴力枚举。就是遍历所有的子串,以样例来说:
-
bar foo the foo bar man (注意步长是单词的长度),从左到右依次遍历,寻找满足条件的子串。
-
我们来思考一下,当该躺不满足条件时:
很明显,无需把right重新移动的到left的位置重新开始,直接继续进行移动就可以。
-
所以此时构成滑动窗口的条件
两个指针移动方向一致
那么我们就按照滑动窗口的解题模版来思考细节:
- 进窗口
- 判断
- 出窗口
- 更新结果(位置待定)
首先我们要解决的是个一般性问题:s 字符串的长度一定是单词的整数倍吗???
显然不是!
那么就要进行多次滑动窗口!保证可以遍历到所有可能的子串。那进行几次呢???
可以看出来只需要进行单词个数次的循环即可!!!再多就发生重复了!
这样大致的框架就有了,剩下的就是然后判断单词是否满足条件。
依旧时候使用哈希算法,利用无向图来模拟哈希表(string 映射 int)。
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
//预备工作
//哈希表1
unordered_map<string,int> hash1;
for(auto ch : words) hash1[ch] ++; //对words进行处理
// 基本变量
int len = words[0].size(); //单词长度(步长)
int m = words.size();//单词个数
int n = s.size();//字符串长度
//答案
vector<int> ans;
//多次进行滑动窗口
for(int i = 0;i < len;i++)
{
//哈希表2 用来进行比较
unordered_map<string,int> hash2;
//进窗口 注意一次移动的步长 注意结束条件(保证最后一个是完整单词,不然会发生越界)
for(int left = i ,right = i ,count = 0; right + len <= n ; right += len )
{
string in = s.substr(right,len);
//进窗口 维护count++ (有效单词数加一)
if(++hash2[in] <= hash1[in])
{
count++;
}
//判断是否超出长度
if(right - left + 1 > m * len)
{
string out = s.substr(left,len);
//出窗口 维护count
if(hash2[out]-- <= hash1[out]) count--;
left += len;
}
//满足条件 更新结果
if(count == m)
{
ans.push_back(left);
}
}
}
return ans;
}
};
我们提交:过啦!!!!
Leetcode 76. 最小覆盖子串
家人们!!! 上链接!!!76. 最小覆盖子串
题目描述
根据题目描述,我们需要再字符串中寻找能够覆盖 t 中所有字符的 最短子串,这个“覆盖”是包含 t 中的每个字母,不用管顺序。来看样例:
- 输入:s = “ADOBECODEBANC”, t = “ABC”
- 输出:“BANC”
- 解释:最小覆盖子串 “BANC” 包含来自字符串 t 的 ‘A’、‘B’ 和 ‘C’。
看了样例,应该就理解了这个“覆盖”:
- 对应字母个数必须大于等于 t 中字母个数
- 可以包含其它字母
算法思路
先来最直接的办法 — 暴力枚举,我们来看暴力算法是如何进行的:
- 首先找到一个包含于 t 的字母
- 然后从这个位置开始遍历
- 直到满足 t 中的每个字母都能在该子串中找到
- 然后再找到下一个包含于 t 的字母 重复 1 - 3
这样就完成了任务,那么如何进行优化呢???
根据暴力的步骤,我们可以看出来两个指针的移动方向是一致的,所以就可以进行滑动窗口算法了。
那么我们就按照滑动窗口的解题模版来思考细节:
- 进窗口
- 判断
- 出窗口
- 更新结果(位置待定)
这个判断要怎样进行判断???
肯定是使用哈希算法,但是如何保证对应字母个数必须大于等于 t 中字母个数
呢???
这一步与之前的判断略有不同,因为我们可以多字母,来看代码:
class Solution {
public:
string minWindow(string s, string t) {
//滑动窗口 + 哈希算法
//准备工作
int kinds = 0; //字母种类 方便判断
int hash1[128] = {0}; //哈希表1 因为只有使用数组比较方便
//遍历所有字母 确定个数
for (auto ch : t)
if (hash1[ch]++ == 0) kinds++;
// 长度
int n = s.size();
//哈希表2 用来比较
int hash2[128] = { 0 };
int begin = -1 ,minlen = INT_MAX;
for(int left = 0,right = 0 ,count = 0;right < n;right++)
{
char in = s[right];
//进窗口 维护count (对应字母相等时更新 保证满足条件 有效字母加一)
if(++hash2[in] == hash1[in]) count++;
//判断
while(count == kinds)
{
//更新结果
if(right - left + 1 < minlen)
{
minlen = right - left + 1;
begin = left;
}
char out = s[left];
//出窗口 维护count (相等时出窗口 有效字母减少)
if(hash2[out]-- == hash1[out]) count--;
left++;
}
}
return begin == -1 ? "" : s.substr(begin,minlen);
}
};
提交:过啦!!!!!!
经过这两道题目的书写,相信大家一定深刻认识到了滑动窗口的使用方法!!!
下面请大家继续刷题吧!!!