LeetCode原题链接:438. 找到字符串中所有字母异位词
下面是题目描述:
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的异位词。
示例 2:
输入: s = “abab”, p = “ab”
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的异位词。
1、解题思路
前言:如果有第一次学习滑动窗口算法的朋友,可以先阅读一下笔者关于滑动窗口算法的第一篇文章:【算法学习】-【滑动窗口】-【长度最小的子数组】,那里对滑动窗口会有较详细的讲解,下面的解题思路中关于相关算法的步骤就仅进行简单的叙述啦。
由题目描述可得, 本题主要可分为以下两个步骤:
(1)判断一个字符串是否为另一个字符串的异位词
这里需要借助哈希表这个数据结构来进行判断,即将两个字符串中的字符分别放入两个哈希表中,然后对比这两个哈希表,若两个哈希表中的字符及字符个数都一样,则说明是异位词;否则不是。
(2)确定滑动窗口
相较于之前笔者有关滑动窗口算法的文章中的滑动窗口,这里的窗口大小是恒定的,即用于构成窗口大小的两个指针是 “共进退” 的。故此时直接照搬之前控制窗口移动的思路反而会使情况变得复杂。下面介绍一下算法的步骤:
- 先初始化两个哈希表,便于直接进行第一次判断
- 判断两个哈希表中的内容否相等,若相等,则记录索引(也就是构成窗口的前面的那个指针的值)
- 接着无论是否相等都需将字符串s对应的哈希表中的第一个字符删除(注意这里要先让数量
--
,数量为0后才执行删除操作)而进行下一次枚举 - 删除后,向s对应的哈希表中插入新的字符,然后两个指针都向后移动一位,准备进行下一次的判断。循环执行上述过程。
2、具体代码
vector<int> findAnagrams(string s, string p)
{
unordered_map<char, int> mapOfp;
unordered_map<char, int> mapOfs;
//初始化哈希表
for (size_t i = 0; i < p.size(); i++)
{
mapOfp[p[i]]++;
mapOfs[s[i]]++;
}
vector<int> res;
size_t cur = p.size();
size_t begin = 0;
while (cur <= s.size())
{
if (mapOfp == mapOfs)
{
res.push_back(begin);
}
if( --mapOfs[s[begin]] == 0)
{
mapOfs.erase(s[begin]);
}
begin++;
mapOfs[s[cur++]]++;
}
if (mapOfp == mapOfs)
{
res.push_back(begin);
}
return res;
}
更新日志:
进一步学习之后,发现这道题还有不少可优化之处和需要注意的地方,请看:
可优化的地方主要分为两个方面:一个是哈希表;另一个是判断两个哈希表是否相等
(1)关于哈希表
如上面具体的解题代码,笔者直接使用了C++STL库中的unordered_map
作为哈希表,虽然也能得到正确答案,但会使整个代码显得比较 “重”;
所以我们可以通过数组下标的直接映射对其进行优化。即:
int hashOfs[26] = {0};
int hashOfp[26] = {0};
- 非常需要注意的一点,也是笔者踩坑的一点:
这里的数组类型应该是int
而不能是char
。
在为字母建立映射时(这里专指用数组进行映射),若仅判断字母 “有没有或在不在” (即对相应下标置一置零问题),那么可以用char
类型数组;但若要涉及到字母数量,则应该用int
类型数组,因为当数据量大时,一个字母的数量可能已超过了char所能表示的数据范围而发生错误。
错误再现:
仅想着字母'a'
会映射在下标为97这个位置,并没想过这个位置的值只能从0~127
。
(2)关于对两个哈希表进行比较
如上面具体的解题代码,笔者也直接用了unordered_map
重载的==
运算符来进行比较,那么它的底层其实就是遍历表中所有节点来判断是否相等,当数据量大时会有一定的时间消耗;
这里的优化方式是采用一个计数变量(下面命名为count)来统计滑动窗口中有效字符的个数,即当前滑动窗口中的字符在子串(也就是本题的p串)中的个数。然后进出窗口的时候维护这个count,用这个count来完成两个哈希表的比较,当有效字符的个数等于p串的长度时,此时窗口中的字符即为一种p串的异位词。下面是具体过程,请看:
在进窗口前,先对p串的哈希表进行初始化工作,遍历p串,将出现的字符在哈希表中对应位置的数量加1,即:
//映射字符串p
for(auto e : p)
{
hashOfp[e - 'a']++;
}
接下来就遍历s串,通过进出窗口来获得问题的解
- 进窗口
由于p串中的字符可能重复,这里每次进窗口的字符可分为三种。第一种,在p串(p串哈希表)中,而全不在或不全在窗口(s串哈希表)中,此时进窗口的字符为有效字符;第二种,是不在p串中,此时为无效字符;最后一种也是关键的一种,在p串中又已经全在窗口中,此时的也为无效字符;
无论是哪种情况,都可以直接让哈希表中对应位置上的值++,++后再去判断字符的有效性,当字符有效时才让count++,可通过以下判断条件来维护进窗口阶段的count:
char in = s[cur++];
if(++hashOfs[in - 'a'] <= hashOfp[in - 'a'])
{
count++;
}
解释:对于第一种,s串哈希表中对应位置上的值++后会等于或小于p串哈希表中对应位置上的值,此时count++
;对于第二种,s串哈希表中对应位置上的值++后会大于p串哈希表中对应位置上的值,此时count不变
;对于第三种,s串哈希表中对应位置上的值++后也会大于p串哈希表中对应位置上的值,此时count也不变
。
- 出窗口
和之前的逻辑一样,当窗口的大小等于p串的大小时即可进行出窗口操作。此时可先进行判断:当有效字符的个数等于p串的长度时,此时窗口中的字符为一种p串的异位词,进行记录。
接着可通过以下判断条件来维护出窗口阶段的count:
char out = s[begin];
if(--hashOfs[out] < hashOfp[out])
{
count--;
}
begin++;
解释:对于第一种,s串哈希表中对应位置上的值--
后会小于p串哈希表中对应位置上的值,此时count--
;对于第二种,s串哈希表中对应位置上的值--
后不会等于p串哈希表中对应位置上的值,此时count不变
;对于第三种,s串哈希表中对应位置上的值--
后不会小于p串哈希表中对应位置上的值,此时count也不变
。
下面是优化后的代码,请看:
vector<int> findAnagrams(string s, string p)
{
int hashOfs[26] = {0}; //注意用char测试用例大了过不了
int hashOfp[26] = {0};
//映射字符串p
for(auto e : p)
{
hashOfp[e - 'a']++;
}
int begin = 0;
int cur = 0;
int count = 0; //有效字符的个数(判断是否可组成异位词)
int kinds = p.size();
vector<int> res;
while(cur < s.size())
{
//进窗口
char in = s[cur++];
if(++hashOfs[in - 'a'] <= hashOfp[in - 'a'])
{
count++;
}
//出窗口
if(cur - begin == p.size())
{
char out = s[begin];
if(count == kinds)
{
res.push_back(begin);
}
if(--hashOfs[out - 'a'] < hashOfp[out - 'a'])
{
count--;
}
begin++;
}
}
return res;
}
看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或看不懂的地方或有可优化的部分还恳请朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹