加个封面
文章目录
1 问什么用双指针,不用dp?
- 对于数组子串这种连续问题解,优先应考虑双指针。
- 没有明显的状态转移,前后的依赖关系,考虑双指针解法。
- 至于不连续的子问题解,即为子序列问题,一般使用动态规划来解决,要求更为苛刻,同时也能解决一些子串问题,详情见另一篇博客。
- 能用双指针不用动态规划,时间复杂度放那了,比如最长回文子序列和最长回文子串。子序列得用动态规划,子串可以用双指针和动态规划,明显双指针更优。
- 本文借鉴于网上资料,再加个人见解。
2 基本框架
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 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++;
// 进行窗口内数据的一系列更新
...
}
}
}
3 子串例题
3.1 最小覆盖子串
分析
- right扩大至windows满足条件,即windows包含三个T中的字母,left缩小windows至刚好满足条件,找到一个字串,记录长度。
- left再缩小windows会破坏满足条件,right就会扩大,再找另一个满足条件的windows
- 具体看代码中的注释
代码
string minWindow(string s, string t) {
//need被需要存的map,window滑动窗口中存的map
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
//window中存的need中的有效字母个数
int valid = 0;
// 记录最小覆盖子串的起始索引及长度
int start = 0, len = INT_MAX;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
//need中存在该字母,则记录到window中,如果该字母个数已经集满,有效值字母数加1
if (need.count(c)) {
window[c]++;
if (window[c] == need[c])
valid++;
}
// 判断左侧窗口是否要收缩
//有效字母数和need中的一致时,满足条件
while (valid == need.size()) {
// 在这里更新最小覆盖子串
//记录最小长度和start点,便于返回
if (right - left < len) {
start = left;
len = right - left;
}
// d 是将移出窗口的字符
//弹出左值
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
//弹出时,是need中的字母,那就更新window中的该字母的个数和总的有效字母数。
//valid的变化会使跳出while,右边界又开始找新的值了
if (need.count(d)) {
if (window[d] == need[d])
valid--;
window[d]--;
}
}
}
// 返回最小覆盖子串
return len == INT_MAX ?
"" : s.substr(start, len);
}
3.2 字符串排序
- 排列必须是连续的。
- window的长度必须保持和字串的长度一致,思想和上一题基本一致。
- 不同点在于窗口收缩的时机判断。
代码
// 判断 s 中是否存在 t 的排列
bool checkInclusion(string t, string s) {
unordered_map<char, int> 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;
}
3.3 找所有字母的异位词
- 找到子排列数组后记录起始点就可。
- 找子排列就是上面一题。
代码
vector<int> findAnagrams(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
vector<int> 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;
}
3.4 最长无重复子串
分析
- 无重复时候right扩张
- 有重复时候left收缩,
- 记录最大值
代码
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> 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;
}
4 链表中的双指针
链表相关博文:⛔⛔⛔数据结构——链链链链表表表表