思路一:滑动窗口法
遍历字符串 s
时,使用与字符串 p
长度相同的滑动窗口,并记录滑动窗口内每个字符的出现次数。将滑动窗口中的字符频率与字符串 p
的字符频率进行比较。如果两者一致,则将当前滑动窗口的起始索引记录为结果;否则继续移动滑动窗口,直到字符串 s
遍历完毕。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int s_len = s.size();
int p_len = p.size();
//当字符串 s 的长度小于字符串 p 的长度时,
//字符串 s 中一定不存在字符串 p 的异位词
//此时返回结果为空的数组
if(s_len < p_len){
return vector<int>();
}
vector<int> result;//存储满足条件的索引结果
vector<int> s_count(26);//记录26个字母在滑动窗口中的出现次数
vector<int> p_count(26);//记录26个字母在字符串p中的出现次数
//处理第一个滑动窗口
for(int i = 0; i < p_len; i++){
++s_count[s[i] - 'a'];
++p_count[s[i] - 'a'];
}
//如果第一个滑动窗口就是p的字母异位词,将滑动窗口左边界索引加入结果
if(s_count == p_count){
result.emplace_back(0);
}
//继续检查滑动窗口内字符是否满足要求
//i为滑动窗口左边界,由于滑动窗口整体都不能超过字符串s的长度
//因此循环需要限定i < s_len - p_len
for(int i = 0; i < s_len - p_len; i++){
--s_count[s[i] - 'a'];//滑动窗口右移,擦除滑动窗口外元素
++s_count[s[i + p_len] - 'a'];//处理因移动滑动窗口而添加至滑动窗口的字符
if(s_count == p_count){
result.emplace_back(i + 1);
}
}
return result;
}
};
- 时间复杂度: O(p.size()+(s.size()−p.size())×Σ)。
解释:两部分:第一部分为滑动窗口初始化字符出现数组的时间复杂度,第二部分是移动滑动窗口的时间复杂度
- 空间复杂度: O(Σ)。
因为只需要存储Σ个字符出现的次数因此空间复杂度为O(Σ)
使用哈希表替代数组:
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int p_len = p.size();
int s_len = s.size();
//哈希表的键为字符,值为字符出现次数
unordered_map<char, int> p_count;//用来存储p中每个字符出现的次数
unordered_map<char, int> s_count;//用来存储滑动窗口中每个字符出现的次数、
//初始化存储p中每个字符出现的次数的哈希表
for(int i = 0; i < p_len; i++){
p_count[p[i]]++;
}
//处理第一个滑动窗口
for(int i = 0; i < p_len; i++){
s_count[s[i]]++;
}
vector<int> result;
//如果两个哈希表中相同,就将滑动窗口左边界索引存入结果数组中
if(p_count == s_count){
result.push_back(0);
}
//继续处理其他滑动窗口
for(int i = 1; i <= s_len - p_len; i++){
//在哈希表中移除滑动窗口左边界元素
s_count[s[i - 1]]--;
//移除出现次数为0的字符对应的键对
if (s_count[s[i - 1]] == 0) {
s_count.erase(s[i - 1]);
}
//在哈希表中加入滑动窗口右边界元素
s_count[s[i + p_len - 1]]++;
if(p_count == s_count){
result.push_back(i);
}
}
return result;
}
};
- 时间复杂度: O(p.size()+(s.size()−p.size())×p.size())。
解释:两部分:第一部分为滑动窗口初始化字符出现数组的时间复杂度,第二部分是移动滑动窗口的时间复杂度
- 空间复杂度: O(p.size())。
因为只需要存储Σ个字符出现的次数因此空间复杂度为O(Σ)
虽然在这个方法中在使用了哈希表代替方法一中的数组后时空复杂度变小了(常数级),但是在判断p中字符出现次数和滑动窗口内字符出现次数时的判断语句严格意义上讲是错误的,原因是:
在C++中,直接使用
p_count == s_count
来判断两个unordered_map
是否相等是不正确的。unordered_map
的相等运算符==
比较的是两个unordered_map
容器的迭代器范围,即它们的键值对是否完全相同,包括顺序。由于unordered_map
是基于哈希表实现的,其元素的顺序是不确定的,因此即使两个unordered_map
包含相同的键值对,它们的顺序也可能不同,从而导致比较结果为false
。
由于哈希表的特殊性,在移除滑动窗口左边界字符时,如果该字符在窗口中不再出现,需要使用 erase 方法从哈希表中删除对应的键值对,以确保哈希表中不包含任何不再需要的元素。
//移除出现次数为0的字符对应的键对
if (s_count[s[i - 1]] == 0) {
s_count.erase(s[i - 1]);
}
思路二:优化的滑动窗口法
与上述滑动窗口方法不同之处在于不再分别统计滑动窗口和字符串 p 中每种字母的数量,而是统计滑动窗口和字符串 p 中每种字母数量的差
在 滑动窗口算法 中,
count
数组记录的是当前窗口内字符频率相对于目标字符串p
中字符频率的差值。具体来说:
count[c - 'a']
的意义:
count[c - 'a']
表示字符c
在当前滑动窗口中出现的次数减去它在字符串p
中的次数。- 初始时,
count
数组根据p
的字符频率设置,例如,如果p
中某个字符c
出现 2 次,count[c - 'a']
会从窗口中每出现一次该字符逐渐向 0 靠拢。举例说明
假设:
p = "abc"
,s = "abcbac"
,窗口长度为 3,初始窗口为s[0:3] = "abc"
。初始化时,
count
表示字符频率差值:
count['a' - 'a'] = 0
(窗口和p
中字符a
数量相同)
count['b' - 'a'] = 0
(窗口和p
中字符b
数量相同)
count['c' - 'a'] = 0
(窗口和p
中字符c
数量相同)现在移除
s[0] = 'a'
:
count['a' - 'a'] = 1
表示窗口中比p
中多了 1 个a
。当移除
s[0]
时:移除前,
count['a' - 'a'] == 1
,说明a
的数量与p
中一致。移除后,
count['a' - 'a']
变成0
,说明窗口中少了a
,即变得不一致。因此,
count[s[i] - 'a'] == 1
表示字符s[i]
的数量在移除之前正好是“与p
中一致的状态”,移除后会导致数量不一致。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int p_len = p.size();
int s_len = s.size();
if(p_len > s_len){
return vector<int>();
}
vector<int> result;
vector<int> count(26);
//记录26个字母出现的次数,
//若记录为0,则说明某个字符在字符串s和字符串p中出现次数一致
for(int i = 0; i < p_len; i++){
++count[s[i] - 'a'];//如果s中出现该字母,就在计数器上加一
--count[p[i] - 'a'];//如果p中出现该字母,就在计数器上减一
}
//循环检查26个字母在出现次数上的差值
int differ = 0;//记录不匹配字符的个数
for(int j = 0; j < 26; j++){
if(count[j] != 0){
differ++;
}
}
if(differ == 0){
result.emplace_back(0);
}
for(int i = 0; i < s_len - p_len; i++){
// 移出窗口左端的字符 s[i],更新其在 count 数组中的数量差异
// 移除前的状态:窗口中该字符比 p 中多了 1 个(数量差异为 1)。
// 移除后的状态:当前窗口中该字符的数量应与和 p 完全一致。
if (count[s[i] - 'a'] == 1) {
--differ;
}
// 移除前的状态:窗口中该字符的数量与 p 完全一致。
// 移除后的状态:窗口中该字符应比 p 中多 1 个。
else if (count[s[i] - 'a'] == 0) {
++differ;
}
//擦除索引为i的字符在count数组中的记录
--count[s[i] - 'a'];
//移入窗口右端的新字符 s[i + pLen],更新其在 count 数组中的数量差异
// 窗口中字母 s[i+pLen] 的数量与字符串 p 中的数量从不同变得相同
if (count[s[i + p_len] - 'a'] == -1) {
--differ;
}
// 窗口中字母 s[i+pLen] 的数量与字符串 p 中的数量从相同变得不同
else if (count[s[i + p_len] - 'a'] == 0) {
++differ;
}
//增加索引为i+p_len的字符在count数组中的记录
++count[s[i + p_len] - 'a'];
if(differ == 0){
result.emplace_back(i + 1);
}
}
return result;
}
};
- 时间复杂度:O(s.size()+p.size()+Σ)。
- 空间复杂度: O(Σ)
总结:
滑动窗口的核心思路是用左右两个指针动态地调整窗口的范围,具体包括根据需要不断地从左侧移除元素(收缩窗口)或从右侧加入元素(扩张窗口)。即便是在定长滑动窗口的应用场景中,我们仍然需要遵循这种动态维护的方式,而不是单纯在固定区间内来回移动一个长度不变的窗口。