主要思想
两个滑动窗口的要点:
-
单调性:数据在某一方向上呈现某一种单调性,此时滑动窗口移动的时候才是有依据的。
-
左右两个指针:两个指针分别是滑动窗口的两个边,移动指针控制窗口大小。
力扣 209. 长度最小的子数组
解题思路:定义两个指针 left 和 right,都从 0 开始移动。
先移动 right,每次向右移动都记录此时窗口内数据和,停止条件是 sum >= target。
此时由于数据的单调性(全是正数,相加一定越来越大),将 left 向右移动一次之后,不用再将 right 从头开始移动,只需将 sum -= nums[left] 即可。
代码:
class Solution
{
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int n = nums.size(), sum = 0, ret = INT_MAX;
for (int left = 0, right = 0; right < n; right++)
{
sum += nums[right];
while (sum >= target)
{
ret = min(ret, right - left + 1);
sum -= nums[left++];
}
}
return ret == INT_MAX ? 0 : ret;
}
};
时间复杂度:虽然有两层循环,但是最坏情况是:right 先移动到最后,left 再移动到最后,整个过程中 right 不会回退,所以时间复杂度是 O(n)。
力扣 3. 无重复字符的最长子串
对于这道题,也满足滑动窗口的两个要点,双指针显而易见,单调性是指:指针 right 向右移动过程中,任意一个字符的数目只可能增多或者不变(滑动窗口内),而 left 右移过程中,任意字符数量只可能不变或者减少。
所以,要借助一个哈希表来记录窗口内,任意时刻,每个字符的个数。
然后就是 right 向右,进窗口,字符数量增加;left 向右,出窗口,字符数量减少。
代码:
class Solution
{
public:
int lengthOfLongestSubstring(string s)
{
int n = s.size(), ret = 1;
vector<int> hash(128, 0);
for (int left = 0, right = 0; right < n; right++)
{
hash[s[right]]++;
while (hash[s[right]] > 1) hash[s[left++]]--;
ret = max(ret, right - left + 1);
}
return n == 0 ? 0 : ret;
}
};
时间复杂度还是 O(n)。
力扣 1004. 最大连续 1 的个数(三)
这个题目需要转换一下解决办法,才能用滑动窗口。
保证找到一个最长的区间,其中的 0 的个数不超过 k 即可。
所以单调性指的就是,滑动窗口中 0 的个数,right 向右移动只会不变或增加,left 向右移动只会不变或减少。
代码:
class Solution
{
public:
int longestOnes(vector<int>& nums, int k)
{
int n = nums.size(), ret = 0, num = 0;
for (int left = 0, right = 0; right < n; right++)
{
if (nums[right] == 0) num++;
while (num > k)
{
if (nums[left++] == 0)
{
num--;
}
}
ret = max(ret, right - left + 1);
}
return ret;
}
};
力扣 1658. 将 x 减到 0 的最小操作数
还是得转换,只需要找到一个最长的区间,使得其中的数字之和等于 整个数组的和减去 x,则这个区间之外的数字的个数就是最小操作数。
代码:
class Solution
{
public:
int minOperations(vector<int>& nums, int x)
{
int n = nums.size(), sum = 0, len = -1;
for (int i = 0; i < n; i++) sum += nums[i];
int target = sum - x;
if (target < 0) return -1;
for (int left = 0, right = 0, tmp = 0; right < n; right++)
{
tmp += nums[right];
while (tmp > target) tmp -= nums[left++];
if (tmp == target) len = max(len, right - left + 1);
}
return len == -1 ? len : n - len;
}
};
力扣 904. 水果成篮
主要就是寻找一个最长的区间,使得其中的数字不超过两种。
所以单调性就是,在一段区间中,不同数字的个数。right 向右移动过程中,区间中的数目只会不变或增加;left 向右移动过程中,区间中的数目只会不变或减少。
class Solution
{
public:
int totalFruit(vector<int>& fruits)
{
unordered_map<int, int> hash;
int n = fruits.size(), ret = 0;
for (int left = 0, right = 0; right < n; right++)
{
hash[fruits[right]]++;
while (hash.size() > 2)
{
hash[fruits[left]]--;
if (hash[fruits[left]] == 0) hash.erase(fruits[left]);
left++;
}
ret = max(ret, right - left + 1);
}
return ret;
}
};
力扣 438. 找到字符串中所有字母异位词
这道题能用滑动窗口的一个关键点是,字母异位词只要求每一个出现的字母的个数相等,而不要求顺序。
所以就可以转换为找到几个区间,满足其中每个字母出现的次数和目标字符串相等,即单调性。
class Solution
{
public:
vector<int> findAnagrams(string s, string p)
{
vector<int> ret;
vector<int> hash1(26, 0), hash2(26, 0);
for (char c : p) hash1[c - 'a']++;
int m = p.size(), n = s.size();
for (int left = 0, right = 0, count = 0; right < n; right++)
{
char in = s[right];
// 进窗口 + 维护 count
if (++hash2[in - 'a'] <= hash1[in - 'a']) count++;
if (right - left + 1 > m)
{
char out = s[left++];
// 出窗口 + 维护 count
if (hash2[out - 'a']-- <= hash1[out - 'a']) count--;
}
if (count == m) ret.push_back(left);
}
return ret;
}
};
力扣 30. 串联所有单词的子串
// 只要求包含所有单词并且没有多余的单词, 而且顺序可以不同, 所以这道题里应该看重每一个单词的数量
// 但如果真的每次向后移动一位就判断一下是不是 words 里的某一个单词, 必然会超时
// 所以可以借助一个哈希表进行优化, 就可达到一个类似于"降维"的效果, 将每一个单词看成一个字符
// 然后利用滑动窗口的思想向后移动并判断字符的数量是否一致
// 这里要注意, 有可能出现从第一个字母开始不符合要求, 但从第二个或第三个...开始就符合要求了
// 所以, 一次遍历还是无法得到正确答案, 应该遍历 words[0].size() 次
class Solution
{
public:
vector<int> findSubstring(string s, vector<string>& words)
{
vector<int> ret;
unordered_map<string, int> hash1;
for (string word : words) hash1[word]++;
int m = words.size(), n = s.size(), len = words[0].size();
for (int i = 0; i < len; i++) // 确保不会遗漏
{
unordered_map<string, int> hash2; // 窗口内单词的个数
for (int left = i, right = i, count = 0; right + len <= n; right += len)
{
// 进窗口 + 维护 count
string in = s.substr(right, len);
if (++hash2[in] <= hash1[in]) count++;
// 判断需不需要出窗口
if (right - left + 1 > len * m)
{
// 出窗口 + 维护 count
string out = s.substr(left, len);
if (hash2[out]-- <= hash1[out]) count--;
left += len;
}
if (count == m) ret.push_back(left);
}
}
return ret;
}
};
力扣 76. 最小覆盖子串
// 这道题如果要用滑动窗口的思想解决, 需要注意一个点
// 寻找的区间中不止要满足字符数目相等, 字符的种类也要相同, 每种字符的个数也要相同
// 所以还要使用哈希表来优化, 以及使用两个变量来标记种类和数量
class Solution
{
public:
string minWindow(string s, string t)
{
vector<int> hash1(128, 0), hash2(128, 0);
int kinds = 0, len = INT_MAX, begin = -1, n = s.size();
for (char c : t)
{
if (hash1[c]++ == 0) kinds++;
}
for (int left = 0, right = 0, count = 0; right < n; right++)
{
// 进窗口 + 维护 count
char in = s[right];
if (++hash2[in] == hash1[in]) count++;
// 在已经满足条件的情况下, 寻找最短长度
while (count == kinds)
{
// 更新结果
if (right - left + 1 < len)
{
len = right - left + 1;
begin = left;
}
// 出窗口 + 维护 count
char out = s[left++];
if (hash2[out]-- == hash1[out]) count--;
}
}
return begin == -1 ? "" : s.substr(begin, len);
}
};