题目:
给定两个字符串 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 <= s.length, p.length <= 3 * 104
s
和p
仅包含小写字母
解法一(哈希表计数):
利用滑动窗口的思想并结合哈希表计数的方式实现异位位的查找,其中滑动窗口思想主要是通过每次增加一个元素并减少一个元素来降低时间复杂度(不需要再重新遍历全部所有的值)。除此之外通过计数是否小于0来判断两个哈希表是否相同不如直接通过hashTable1==hashTable2来判断更加简介,如下为笔者代码:
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int length_s = s.size();
int length_p = p.size();
vector<int> result;
int number=length_p;
unordered_map<char,int> hashTable1;
for(int i=0;i<length_p;i++){
hashTable1[p[i]]++;
}
int left = 0;
int right = length_p;
int fuhao = 0;
for(int i=0;i<length_p;i++){
auto it = hashTable1.find(s[i]);
if(it!=hashTable1.end()){
hashTable1[s[i]]--;
if(hashTable1[s[i]]==-1){
fuhao++;
number--;
continue;
}
if(hashTable1[s[i]]<0){
number--;
continue;
}
number--;
}
}
if(number==0 && fuhao==0){
result.push_back(left);
}
left++;
while(right<length_s){
auto it = hashTable1.find(s[left-1]);
if(it!=hashTable1.end()){
hashTable1[s[left-1]]++;
number++;
if(hashTable1[s[left-1]]==0){
fuhao--;
}
}
auto it_right = hashTable1.find(s[right]);
if(it_right!=hashTable1.end()){
hashTable1[s[right]]--;
if(hashTable1[s[right]]==-1){
left++;
right++;
fuhao++;
number--;
continue;
}
if(hashTable1[s[right]]<0){
left++;
right++;
number--;
continue;
}
number--;
}
if(number==0 && fuhao==0){
result.push_back(left);
}
left++;
right++;
}
return result;
}
};
解法二(滑动窗口-数组计数):
根据题目要求,我们需要在字符串s寻找字符串p的异位词。因为字符串p的异位词的长度一定与字符串p的长度相同,所以我们可以在字符串s中构造一个长度为与字符串p的长度相同的滑动窗口,并在滑动中维护窗口中每种字母的数量;当窗口中每种字母的数量与字符串p中每种字母的数量相同时,则说明当前窗口为字符串p的异位词。
在算法实现中,我们可以使用数组来存储字符串p和滑动窗口中每种字母的数量。
当字符串s的长度小于字符串p的长度时,字符串s中一定不存在字符串p的异位词。但是因为字符串s中无法构成长度与字符串p的长度相同的窗口,所以这种情况需要单独处理。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int sLen = s.size(), pLen = p.size();
//如果s的长度小于p的长度,则直接返回空vector<int>();
if (sLen < pLen) {
return vector<int>();
}
//通过vector<int>数组来计数,便于直接进行数组是否相等的判断
//先加入索引位置为0的s和p的字符串值进行初始化
vector<int> ans;
vector<int> sCount(26);
vector<int> pCount(26);
for (int i = 0; i < pLen; ++i) {
++sCount[s[i] - 'a'];
++pCount[p[i] - 'a'];
}
if (sCount == pCount) {
ans.emplace_back(0);
}
//利用滑动窗口的思想,新增首位的字符并删除末尾的字符的方式进行滑动
for (int i = 0; i < sLen - pLen; ++i) {
--sCount[s[i] - 'a'];
++sCount[s[i + pLen] - 'a'];
if (sCount == pCount) {
ans.emplace_back(i + 1);
}
}
return ans;
}
};
时间复杂度:O(m+(n−m)×Σ),其中 n 为字符串 s 的长度,m 为字符串 p 的长度,Σ 为所有可能的字符数。我们需要 O(m) 来统计字符串 p 中每种字母的数量;需要 O(m) 来初始化滑动窗口;需要判断 n−m+1 个滑动窗口中每种字母的数量是否与字符串 p 中每种字母的数量相同,每次判断需要 O(Σ) 。因为 s 和 p 仅包含小写字母,所以 Σ=26。空间复杂度:O(Σ)。用于存储字符串 p 和滑动窗口中每种字母的数量。
笔者小记:
1、滑动窗口思想及其优势:滑动窗口是一种常用于处理数组或序列类问题的算法思想。它的核心思想是利用两个指针(通常是左指针和右指针)来维护一个窗口范围,并随着窗口的滑动来更新答案。滑动窗口能够在很多场景下优化时间复杂度,尤其是对于需要遍历和比较多个子数组或子序列的问题;减少冗余计算:通过添加右指针首部新的元素并同时剔除左指针指向的末尾元素,避免了重复遍历数组或子数组的情况,减少了不必要的计算量,通常能够将时间复杂度从O (n^2)降到 O(n)。
2、数组和哈希表计数的方式:可以通过将哈希表和数组计数的方式,可以通过直接判断是否相等(hashTable1==hashTable2;vector<int> sCount==vector<int> pCount)来确定两个哈希表或数组是否相同。