这几道题可以说是有一点难度的,但是掌握方法以后可以说非常简单了;
一、找到字符串中所有字母异位词
题目解析

题目给定了两个字符串
s
和p
,让我们在s
中找到p
的异位词的字串,并且返回这些字串的索引**异位词:**简单来说就是字母组成相同,位置不同;就比如
cba
是abc
的异位词。这里我们要找
s
的异位词 ,那我们要找的子串长度一定等于s
的长度
算法思路
这里看到这道题要找到子串,我们首先想到的肯定是暴力解法:枚举所有长度和s
相等字符串,找到满足条件的字符串然后返回
这里对于枚举字符串,我们可以进行一下优化:
对于暴力解法,固定一个位置
left
,让right
向后遍历,找到满足条件的子串时(如下图所示:)此时是满足条件的(当前区间
[left , right]
的子串是p
的异位词) ,那区间[left+1, right]
就一定不是满足条件的(异位词要求字符的组成是相同的) ,所以我们就让left++
后,就不必让right
再从left
开始向后遍历,而是继续从right
当前的位置向后遍历。这是,
left
和right
就是一个同向双指针,我们就非常好想到要用滑动窗口来来解决问题了。

知道了这道题,我们要用同向双指针(滑动窗口),
那如何记录区间[left , right]
内出现的字符串呢?
这里通过看题目,我们会发现:我们不仅要记录字符的种类,还要记录每一种字符的数量 ,这里就要使用
hash
表来记录。
那现在来看如何使用滑动窗口呢?
首先就是
right
向后进行遍历,进行入窗口操作。然后就是出窗口,那该什么时候进行出窗口操作呢?又该如何进行出窗口操作呢?
- 什么时候出窗口?:我们通过看题可以发现,我们要找的子串长度肯定等于字符串
s
的长度 ,所以我们出窗口操作要在[left , right]
长度大于s
的长度之后再出窗口。- 那如何出窗口呢?:这里出窗口操作很简单,就是将
left
位置的字符的数量-1
即可。那什么时候更新结果呢?
更新结果,那肯定是满足条件的时候去更新,要想满足条件,区间
[left , right]
长度肯定要等于p
的长度且区间内出现字符的种类和数量要和p
相等。(那这里我们也要使用hash
来记录p
字符串中出现字符的种类和数量)。
通过上述分析,我们要使用两个hash
表来记录p
和区间[left , right]
出现字符的种类和数量。
这里我们思考一个问题:如何判断两个hash
表中字符种类和数量是否相等?
看到这里可能会疑惑,直接去遍历两个
hash
表,判断每一个字符出现的次数是否相等不就好了;遍历
hash
表去判断每一个字符是否相等确实可以,但未免有些太麻烦了,有没有更加简单又好理解的方法?有的兄弟有的 ,我们不想要使用
hash
表去比较,那我们可以使用一个count
来计数;(count
记录的是有效字符的个数)当区间长度等于
p
的长度且count
等于p
的长度,那当前区间就是p
的子串;我们只需要在入窗口和出窗口时,进行一下
count
的更新即可
什么意思呢?
我们使用
count
来记录区间[left , right]
内有效字符的个数;那在如窗口和出窗口操作时如何更新呢?
在入窗口时: 我们将
right
位置字符放入hash2
后,如果hash2[s[right]]
<=hash1[s[right]]
,那就说明right
位置的字符就是有效字符,我们就让count++
;在出窗口时: 我们将
left
位置字符移除hash2
,如果hash2[s[left]]
<hash1[s[left]]
时,那就说明left
位置的字符是有效字符,我们就让count--
。
那现在我们大体思路就理清楚了,现在来看整体的过程
现将
p
中字符放入hash1
;
- 入窗口:将
right
位置的字符放入hash2
;如果hash2[s[right]] <= hash1[s[right]]
,那就让count++
;- 出窗口:当区间长度大于
p
的长度时,进行出窗口操作;将left
位置的字符移出hash2
(让hash[s[left]]--
即可),如果hash2[s[left] < hash1[s[left]]
,那就让count--
;- 更新结果:因为我们会一直维持取长度,让区间长度等于
p
的长度,所以只需要判断count == p.size()
即可,相等时就更新结果。

代码实现
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int hash1[26] = {0};
int hash2[26] = {0};
for (auto& e : p)
hash1[e - 'a']++;
vector<int> ret;
int left = 0, right = 0, count = 0;
while (right < s.size()) {
// 进窗口
char in = s[right++];
hash2[in - 'a']++;
if (hash2[in - 'a'] <= hash1[in - 'a'])
count++;
// 出窗口
if (right - left > p.size()) {
char out = s[left++];
hash2[out - 'a']--;
if (hash2[out - 'a'] < hash1[out - 'a'])
count--;
}
// 更新结果
if (count == p.size()) {
ret.push_back(left);
}
}
return ret;
}
};
二、串联所有单词的子串
题目解析

对于这道题,不要被它的难度吓到了啊;
题目给我们一个字符串
s
和一个字符串数组words
,其中words
中每一个字符串的长度都是相同的(这个非常重要)。让我们在
s
中找串联子串(words
中所有字符串以任意的顺序排列),最后要返回所有串联子串的索引。
算法思路
这里如果我们直接来看这一道题,难入登天啊,这该咋去找啊?
这里大家可以先去看一下上面那一道找
异位词
的题目 ,看过以后,你会发现一个问题,上面要找的是一个字符串的异位词,那我们这里是不是可以理解成找一个字符数组words
的异位字符串。(把words
中每一个字符串当成一个整体)就比如
words
字符数组是["foo" , "bar"]
,那"foobar"
和"barrfoo"
就是它的异位字符串(按某种顺序排列)。
有了上述的理解,那这道题就简单了许多,但还是存在问题;
words
中的字符串长度都是相等的,假设都等于len
;我们把
words
中的每一个字符串当成一整体,那我们遍历s
的时候,应该如何去划分呢?
我们可以从0、1、2、3...... len-1
位置开始,后面len
个字符作为一个字符串(因为从len
位置开始和从0
位置开始,这样划分是一样的),那我们就一种划分,进行len
次滑动窗口操作,就将所有的情况都计算了在内。

大致思路如上图所示,这里就不在重复了,直接来看代码。
代码实现
cpp
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
unordered_map<string, int> hash1;
for (auto& e : words)
hash1[e]++;
int n = words.size();
int len = words[0].size();
vector<int> ret;
for (int i = 0; i < len; i++) {
int left = i, right = i;
unordered_map<string, int> hash2;
int count = 0;
while (right < s.size()) {
// 进窗口
string in = s.substr(right, len);
hash2[in]++;
right += len;
if (hash1.count(in) && hash2[in] <= hash1[in])
count++;
// 出窗口
if (right - left > n * len) {
string out = s.substr(left, len);
hash2[out]--;
left += len;
if (hash1.count(out) && hash2[out] < hash1[out])
count--;
}
if (count == n)
ret.push_back(left);
}
}
return ret;
}
};
三、最小覆盖子串
题目解析

题目给我们字符串
s
和字符串t
,让我们在s
中找到一个子串,这个子串要包含t
串中的所有字符;然后让我们找到满足条件并且长度最小的子串并返回,如果不存在满足条件的子串,那就返回
""
。
算法思路
这道题,整体思路呢还是和上面类似;
对于暴力解法,就是依次从每一个位置开始找满足条件的最短子串,然后返回长度最小的字串即可。
解法:滑动窗口+hash
统计
首先,我们要先将
t
字符串中所有字符出现的次数统计下来;放到hash1
中。然后我们这里依旧是使用
count
记录有效字符的种类;但是我们这里更新
count
和上面题目中不一样:
- 入窗口时:在如窗口之后,进行判断如果
hash2[in] == hash1[in]
,则表示入的这个字符是一个有效字符,count++
;- 出窗口时:在出窗口操作之前,进行判断,如果
hash2[out] == hash1[out]
,表示我们要出的这个字符是有效字符,count--
。简单来说就是当我们入窗口操作之后,
hash2[in] == hash1[in]
这就说明我们入完这一个字符之后,区间内这个字符出现的次数和t
中这个字符出现的次数相等,就表示区间内有效字符的种类增加了一个。在入窗口操作之前,
hash2[out] == hash1[out]
就说明此时区间内这字符出现的次数和t
中这个字符出现的次数不相等了,我们出完这个字符之后就不相等了;就让区间内有效字符的数量减少了一个。
- 入窗口:将
right
位置的字符放入hash2
中,然后进行更新count
的操作。- 出窗口:当
count == hash1.size()
时,表示当前区间是覆盖了t
的,此时是满足条件的,我们就要进行更新结果;然后在出窗口操作之前更新count
,再执行出窗口操作。- 更新结果:据上面描述,更新结果是在出窗口操作之前的。

代码实现
这里实现代码时,有一个细节,就是我们使用unordered_map
,它的[]
是可以进行插入操作的 ,我们在使用[]
之前(hash1[in]
和hash1[out]
),先进行判断,如果hash1
中存在再进行次数的判断。
cpp
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> hash1;
unordered_map<char, int> hash2;
for (auto& e : t)
hash1[e]++;
int ret = -1, len = s.size() + 1;
int left = 0, right = 0, count = 0;
while (right < s.size()) {
char in = s[right++];
hash2[in]++;
if (hash1.count(in) && hash2[in] == hash1[in])
count++;
while (count == hash1.size()) {
// 更新结果
if (right - left < len) {
ret = left;
len = right - left;
}
char out = s[left++];
if (hash1.count(out) && hash2[out] == hash1[out])
count--;
hash2[out]--;
}
}
if (ret == -1)
return "";
else
return s.substr(ret, len);
}
};
到这里,滑动窗口算法思路的学习就结束了,简单总结:
滑动窗口这一思想,主要应用于我们找满足条件的子串或者子数组
思路很简单,最主要的还是我们需要通过分析,通过对暴力枚举的不断优化,来得出我们滑动窗口这一思路;
到这里本篇文章内容就结束了
感谢各位的支持
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws