Leetcode 找到字符串中所有字母异位词

在 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) { //当且仅当两个向量中每个位置的每个元素都相同时, == 返回trueresult.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 + 1result.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 + 1result.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 对应的是当前滑动窗口的最右边界,而我们需要的是这个窗口的最左边界(即异位词的起始位置)。
