LeetCode 438 Find All Anagrams in a String 题解 - 滑动窗口
题意
给出一个字符串 s 和一个非空字符串 p,找出 s 中所有 p 的 anagram 的下标。输入的字符串中只包含小写字母。
anagram:任意改变原字符串中字符顺序的所有新字符串,包括原串。例如 abc, bac, acb, bca 都是 abc 的 anagram。
思路
特点
不难看出这是一个搜索子串(substring)的问题,只是这些子串比较特殊 —— 是一个子串通过变换字符位置生成的(这不废话吗)。那么我们就可以得到下面这个结论:
如果找到了一个长度和 p 相同的子串 t,并且可以确定,p 中出现的每个字符(包括重复的)t 中都出现,那么 t 就是 p 的一个 anagram。
依照这个思想,我们可以只保留 长度与 p 相同的子串 t 的信息,每次向前移动一个字符,将 t 的尾部字符去掉,将当前字符加入子串 t,然后检查 t 是否为 p 的一个 anagram,重复这个步骤直到 s 末尾。
滑动窗口
滑动窗口其实就像乘火车过安检时候的机器,不停地检查乘客的包裹。 其思想不难理解,不过对于这道题,我们不是让传送带一个一个来传送包包,而是让安检机器(窗口)沿着传送带(数据列表)移动来检查每个包包(数据)。
滑动窗口协议 (Sliding window protocol)
作为TCP协议中很重要的一个协议,滑动窗口协议是这种思想的一种典型应用,具体细节感兴趣的可以参考计算机网络相关书籍。
滑动窗口协议(Sliding Window Protocol),属于TCP协议的一种应用,用于网络数据传输时的流量控制,以避免拥塞的发生。该协议允许发送方在停止并等待确认前发送多个数据分组。由于发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输,提高网络吞吐量。 —— 百度百科
解法
由于 p的 anagrams 中每个字符串包含的字符是一样的,因此,我们可以通过判断当前出现的字符有多少个与 p 中的每个字符相对应,来确定当前子串是否为 p 的 anagram。具体一点,假设 p 为 aab,s 为 abaaa:
- 我们先统计得到,p 的长度为 plen = 3,p 中字符 a 出现的次 times[a] = 2, b 出现的次数 times[b] = 1,其余字符未出现过。
- 然后我们拷贝得到一份 tmptimes[] = times[] 和 tmpplen = plen 用来保存 s 中的字符与 p 中字符的抵消记录。从前往后查看 s 中字符:
- 第一个 a 在 p中出现过,那么抵消一个,将 tmpplen 减一,tmptimes[a]减一;
- 第二个 b 在 p中出现过,则抵消一个,tmpplen减一,tmptimes[b]减一;
- 第三个 a,依旧是 a,前面抵消了一个 a,再抵消一个,将 tmpplen 减一,tmptimes[a]减一,这个时候 tmpplen 为 0 了,那么我们可以确定,得到了一个 p 的 anagram;
- 第四个 a,依旧是 a,由于我们窗口的长度为 3,因此先将第一个的抵消记录清除,tmpplen 加一,tmptimes[a]加一,然后再用这个 a 抵消 p 中的 a,tmpplen 减一,tmptimes[a]减一;tmpplen 为 0,那么这就又是一个 anagram;
- 第五个 a,依旧是 a,同样,窗口长度为3,因此将第二个 b 的抵消记录清除,tmpplen 加一,tmptimes[b]加一,然后,由于 p 中的 a 已经全部被抵消了(tmptimes[a] = 0),所以 a 不能被抵消,而tmpplen也不为0,因此这个子串 —— aaa 并不是 p 的一个 anagrams。
- s没有更多字符,结束。
代码
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
const int N = 26;
// 保存 p 中每个字符是否出现,出现次数,以及总长度
int times[N], tot = 0;
bool occured[N];
memset(occured, 0, sizeof(occured));
memset(times, 0, sizeof(times));
for (char ch : p) {
int u = ch - 'a';
times[u]++;
occured[u] = true;
tot++;
}
vector<int> res;
// 用来抵消的拷贝值
int tmptot = tot, tmptimes[N];
memcpy(tmptimes, times, sizeof(times));
for (int i = 0; i < (int) s.size(); i++) {
// 清除头部的抵消记录,当然如果还没有多余的头则不用清除
if (i - tot >= 0) {
char ch = s[i - tot];
int v = ch - 'a';
// 只考察在 p 中出现过的字符
if (occured[v]) {
// tmptimes[v] < 0 说明这个字符并未对 长度抵消记录 产生影响
if (tmptimes[v] >= 0)
tmptot++;
//
tmptimes[v]++;
}
}
// 添加当前字符 ch 的抵消
char ch = s[i];
int u = ch - 'a';
if (occured[u]) {
// tmptimes[u] == 0 说明 p 中的对应字符都已经抵消了,
// 这个字符也就不应该影响 长度抵消记录 了
if (tmptimes[u] > 0)
tmptot--;
tmptimes[u]--;
}
// 长度抵消记录 = 0,找到了一个 anagram
if (tmptot == 0) {
res.push_back(i - tot + 1);
}
}
return res;
}
};