目录
1、什么时候应该移动 right 扩大窗口?窗口加入字符时,应该更新哪些数据?
2、什么时候窗口应该暂停扩大,开始移动 left 缩小窗口?从窗口移出字符时,应该更新哪些数据?
3、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
/* 滑动窗口算法框架 */
void slidingWindow(string s) {
unordered_map window;
int left = 0, right = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 增大窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
string minWindow(string s, string t) {
unordered_map need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
// 记录最小覆盖子串的起始索引及长度
int start = 0, len = INT_MAX;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 扩大窗口
right++;
// 进行窗口内数据的一系列更新
if (need.count(c)) {
window[c]++;
if (window[c] == need[c])
valid++;
}
// 判断左侧窗口是否要收缩
while (valid == need.size()) {
// 在这里更新最小覆盖子串
if (right - left < len) {
start = left;
len = right - left;
}
// d 是将移出窗口的字符
char d = s[left];
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
if (need.count(d)) {
if (window[d] == need[d])
valid--;
window[d]--;
}
}
}
// 返回最小覆盖子串
return len == INT_MAX ?
"" : s.substr(start, len);
}
// 判断 s 中是否存在 t 的排列
bool checkInclusion(string t, string s) {
unordered_map need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
char c = s[right];
right++;
// 进行窗口内数据的一系列更新
if (need.count(c)) {
window[c]++;
if (window[c] == need[c])
valid++;
}
// 判断左侧窗口是否要收缩
while (right - left >= t.size()) {
// 在这里判断是否找到了合法的子串
if (valid == need.size())
return true;
char d = s[left];
left++;
// 进行窗口内数据的一系列更新
if (need.count(d)) {
if (window[d] == need[d])
valid--;
window[d]--;
}
}
}
// 未找到符合条件的子串
return false;
}
vectorfindAnagrams(string s, string t) { unordered_map need, window; for (char c : t) need[c]++; int left = 0, right = 0; int valid = 0; vector res; // 记录结果 while (right < s.size()) { char c = s[right]; right++; // 进行窗口内数据的一系列更新 if (need.count(c)) { window[c]++; if (window[c] == need[c]) valid++; } // 判断左侧窗口是否要收缩 while (right - left >= t.size()) { // 当窗口符合条件时,把起始索引加入 res if (valid == need.size()) res.push_back(left); char d = s[left]; left++; // 进行窗口内数据的一系列更新 if (need.count(d)) { if (window[d] == need[d]) valid--; window[d]--; } } } return res; }
int lengthOfLongestSubstring(string s) {
unordered_map window;
int left = 0, right = 0;
int res = 0; // 记录结果
while (right < s.size()) {
char c = s[right];
right++;
// 进行窗口内数据的一系列更新
window[c]++;
// 判断左侧窗口是否要收缩
while (window[c] > 1) {
char d = s[left];
left++;
// 进行窗口内数据的一系列更新
window[d]--;
}
// 在这里更新答案
res = max(res, right - left);
}
return res;
}
ListfindRepeatedDnaSequences(String s) { // 先把字符串转化成四进制的数字数组 int[] nums = new int[s.length()]; for (int i = 0; i < nums.length; i++) { switch (s.charAt(i)) { case 'A': nums[i] = 0; break; case 'G': nums[i] = 1; break; case 'C': nums[i] = 2; break; case 'T': nums[i] = 3; break; } } // 记录重复出现的哈希值 HashSet seen = new HashSet<>(); // 记录重复出现的字符串结果 HashSet res = new HashSet<>(); // 数字位数 int L = 10; // 进制 int R = 4; // 存储 R^(L - 1) 的结果 int RL = (int) Math.pow(R, L - 1); // 维护滑动窗口中字符串的哈希值 int windowHash = 0; // 滑动窗口代码框架,时间 O(N) int left = 0, right = 0; while (right < nums.length) { // 扩大窗口,移入字符,并维护窗口哈希值(在最低位添加数字) windowHash = R * windowHash + nums[right]; right++; // 当子串的长度达到要求 if (right - left == L) { // 根据哈希值判断是否曾经出现过相同的子串 if (seen.contains(windowHash)) { // 当前窗口中的子串是重复出现的 res.add(s.substring(left, right)); } else { // 当前窗口中的子串之前没有出现过,记下来 seen.add(windowHash); } // 缩小窗口,移出字符,并维护窗口哈希值(删除最高位数字) windowHash = windowHash - nums[left] * RL; left++; } } // 转化成题目要求的 List 类型 return new LinkedList<>(res); }
Rabin-Karp 算法
// Rabin-Karp 指纹字符串查找算法
int rabinKarp(String txt, String pat) {
// 位数
int L = pat.length();
// 进制(只考虑 ASCII 编码)
int R = 256;
// 取一个比较大的素数作为求模的除数
long Q = 1658598167;
// R^(L - 1) 的结果
long RL = 1;
for (int i = 1; i <= L - 1; i++) {
// 计算过程中不断求模,避免溢出
RL = (RL * R) % Q;
}
// 计算模式串的哈希值,时间 O(L)
long patHash = 0;
for (int i = 0; i < pat.length(); i++) {
patHash = (R * patHash + pat.charAt(i)) % Q;
}
// 滑动窗口中子字符串的哈希值
long windowHash = 0;
// 滑动窗口代码框架,时间 O(N)
int left = 0, right = 0;
while (right < txt.length()) {
// 扩大窗口,移入字符
windowHash = ((R * windowHash) % Q + txt.charAt(right)) % Q;
right++;
// 当子串的长度达到要求
if (right - left == L) {
// 根据哈希值判断是否匹配模式串
if (windowHash == patHash) {
// 当前窗口中的子串哈希值等于模式串的哈希值
// 还需进一步确认窗口子串是否真的和模式串相同,避免哈希冲突
if (pat.equals(txt.substring(left, right))) {
return left;
}
}
// 缩小窗口,移出字符
windowHash = (windowHash - (txt.charAt(left) * RL) % Q + Q) % Q;
// X % Q == (X + Q) % Q 是一个模运算法则
// 因为 windowHash - (txt[left] * RL) % Q 可能是负数
// 所以额外再加一个 Q,保证 windowHash 不会是负数
left++;
}
}
// 没有找到模式串
return -1;
}
参考:东哥的刷题秘籍 嘎嘎好用