在 C++ 中,两个 vector<int>
类型的变量进行 ==
操作时,会逐个比较它们的元素,只有当两个向量的长度相同且每个位置上的元素值都相同时,==
操作才会返回 true
。
因此,在这道题的代码中,sCount == pCount
这一操作会执行如下步骤:
- 长度比较:首先,两个
vector<int>
的长度会进行比较。如果长度不相等,直接返回false
。 - 元素逐个比较:如果长度相等,那么会逐个比较两个向量对应位置的元素。如果每个位置上的元素值都相同,则返回
true
;如果某个位置的元素不相等,则立即返回false
。
在这道题中,sCount
和 pCount
的长度始终固定为 26(对应 26 个小写字母),因此不会出现长度不同的情况。==
操作会逐个比较这 26 个元素的值,当 sCount
和 pCount
相等时,意味着当前窗口中的字符频率与字符串 p
中字符的频率完全一致,即找到了一个异位词。
具体执行流程:
- 对于每次
sCount == pCount
,程序将比较两个vector
的 26 个位置。 - 如果所有位置的值都相同,说明这两个频率数组是一样的,那么就返回
true
。 - 如果有一个位置的值不同,则返回
false
。
vector
的 ==
操作的时间复杂度:
在这个特定例子中,sCount
和 pCount
的长度固定为 26,所以比较操作是常数时间 O(26),即 O(1)。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> result;
//如果s的长度都小于p,那么没有可能的异位词,应该直接返回空结果。
if (s.size() < p.size()) return result;
//由于字符串仅仅包含小写字母,所以我们可以创建2个元素个数为26的向量来代替hash表
//我们可以在这两个向量中统计p的字符频率和s中滑动窗口中的字符频率
vector<int> pCount(26, 0);
vector<int> sCount(26, 0);
//然后分别统计p中的字符频率和第一个滑动窗口中的字符频率
for(char c:p) {
pCount[c - 'a']++;
}
int windowSize = p.size();
for(int i = 0; i < windowSize; ++i) {
sCount[s[i] - 'a']++;
}
//如果p的字符频率和第一个窗口中的字符频率相同,则将下标0加入到result数组中。
if(pCount == sCount) { //当且仅当两个向量中每个位置的每个元素都相同时, == 返回true
result.push_back(0);
}
//然后开始滑动窗口, 这里 i 是滑动窗口的最右下标
for(int i = windowSize; i < s.size(); ++i) {
sCount[s[i] - 'a']++;
sCount[s[i - windowSize] - 'a']--; //之前窗口最左边的元素删除
if(sCount == pCount) {
// i - windowSize + 1 是窗口起始下标,
//因为 i - 起始下标 + 1 = windowSize; 所以窗口起始下标 = i - windowSize + 1
result.push_back(i - windowSize + 1);
}
}
return result;
}
};
算法代码中,最巧妙的地方是通过这两行代码,将字符串p和窗口中的子串每个字符存储在下标相同的数组位置上,这使得可以通过==操作符来判断是否是异位词
pCount[c - 'a']++;
sCount[s[i] - 'a']++;
是的,你理解得非常准确!这两行代码是该算法的关键部分之一,使用了字符的 ASCII 码来将字符映射到数组的正确位置上。
具体来说:
- 在
pCount[c - 'a']++
中,c - 'a'
计算的是字符c
到字符'a'
的偏移量。由于小写字母的 ASCII 码是连续的(‘a’ 的 ASCII 码最小,其他字母依次递增),所以通过c - 'a'
可以将字母'a'
对应到数组的索引 0,字母'b'
对应到索引 1,依次类推直到'z'
对应到索引 25。 pCount
和sCount
都是长度为 26 的数组,每个元素表示相应字母的出现频率。例如,pCount[0]
代表字符'a'
在字符串p
中出现的次数,pCount[1]
代表字符'b'
的出现次数,依次类推。- 对于
sCount[s[i] - 'a']++
,它将字符串s
中当前窗口的字符同样映射到对应的数组位置,统计每个字符在窗口内出现的次数。
巧妙之处:
-
数组映射代替哈希表:因为只有 26 个小写字母,所以我们用一个长度为 26 的数组(
vector<int>
)来代替哈希表,这样避免了复杂的哈希映射,同时通过数组的下标操作可以快速访问字符对应的位置,节省了时间。 -
字符频率比较:通过将两个字符串的字符频率存储在数组中的对应位置(下标),我们可以直接利用
==
操作符逐个比较两个频率数组,判断当前窗口中的子串是否是p
的异位词。这种方式比使用排序或者逐个字符比较更加高效,特别适合字符种类较少的情况。
总结:
这两行代码的巧妙之处在于将字符直接映射到数组下标,从而可以用数组来记录字符出现的频率,并通过简单的数组比较来判断异位词,从而使得算法简洁、高效。
为什么这一行代码的下标是i - windowSize + 1而不是i - windowSize?
if(sCount == pCount) {
// i - windowSize + 1 是窗口起始下标,
//因为 i - 起始下标 + 1 = windowSize; 所以窗口起始下标 = i - windowSize + 1
result.push_back(i - windowSize + 1);
}
这行代码中的下标 i - windowSize + 1
是用来计算异位词的起始位置。让我们一步步来理解为什么这个下标需要加 1
。
滑动窗口的工作原理:
假设 p
的长度是 windowSize
,每次滑动窗口检查的都是一个长度为 windowSize
的子串。
- 当
i
为当前窗口的最右端下标时,窗口的左端下标就是i - windowSize + 1
,这样窗口的长度恰好为windowSize
。
举个例子:
假设字符串 s = "abab"
, p = "ab"
,则 windowSize = 2
,我们看看当 i
变化时,窗口的边界情况:
- 当
i = 1
时,当前窗口的字符是"ab"
(s[0]
到s[1]
),它是从i - windowSize + 1 = 1 - 2 + 1 = 0
开始的。 - 当
i = 2
时,当前窗口的字符是"ba"
(s[1]
到s[2]
),它是从i - windowSize + 1 = 2 - 2 + 1 = 1
开始的。
可以看到,加上 +1
是为了得到窗口的起始位置,因为 i
是当前窗口的最右端索引,而我们需要将这个窗口的最左端索引加入到结果中。
更详细的解释:
i
是当前窗口的 最右端 的索引(即当前我们处理的字符的索引)。windowSize
是窗口的长度,所以窗口的左端应该是i - windowSize + 1
。- 如果我们直接用
i - windowSize
,得到的是窗口左端 之前 的一个位置,不是实际的起始位置。因此,需要加1
来得到准确的左端起始位置。
总结:
i - windowSize + 1
的作用是从当前窗口的最右端 i
推导出该窗口的 左端起始位置,并将这个起始位置存入结果中。这是因为 i
对应的是当前滑动窗口的最右边界,而我们需要的是这个窗口的最左边界(即异位词的起始位置)。