目录
例题:
概念
滑动窗口,就是一个会滑动的窗口...呃....这么说估计会被骂....但是,算法不是公式,也不是套路,不是一个模板。而是一种思路,一种优化解决问题的思路。所以,生硬的去总结和概括,那是生硬的,难以接受的,学不会的。不同的算法,是对应不同的解决问题的方法策略。一个问题的解决,其策略是唯一的吗?不是,只有比较合适,或者比较不合适。所以,算法无常势。某个具体的算法,你需要学会的是其思想,其解决问题的切入点。我们要培养的,是多角度的切入点,多策略的思考。当你学了某个算法,你能对这种解决方式有个底。那么当你下次再遇到类似的问题,你就会敏感,就会触类旁通。然后根据思想,设计具体的解题过程。
所以,下面跟我来做一个系列的题目,这一系列的题目都将使用滑动窗口的思路进行解决,自己体会。注意我是如何分析,如何解决,如何根据思路实现代码的转换。
一、长度最小的子数组
LCR 008. 长度最小的子数组 - 力扣(LeetCode)
思路:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
int left = 0, right = 0;
int len = n, sum = 0;
while (right < n)
{
sum += nums[right];
while(sum >= target)
{
sum -= nums[left];
len = min(len,right - left + 1);
++left;
}
++right;
}
if(left == 0)
{
return 0;
}
return len;
}
};
二、无重复字符的最长字串
LCR 016. 无重复字符的最长子串 - 力扣(LeetCode)
思路:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0, right = 0, n = s.size();
int hash[128] = {0};
int len = 0;
while(right < n)
{
//进入窗口
hash[s[right]]++;//记录进入的值
while(hash[s[right]] > 1)//判断是否重复,重复就是2
{
//删除对应的值
hash[s[left]]--;
++left;
}
//到这里,说明删除重复值完毕,继续走
len = max(len, right - left + 1);
++right;
}
return len;
}
};
三、最大连续1的个数 III
1004. 最大连续1的个数 III - 力扣(LeetCode)
思路:题目转化为:寻找最大子数组,但是0的个数小于k
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
//最大子数组,0不超过k
//滑动窗口解决
int left = 0, right = 0, len = 0,zero = 0, n = nums.size();
while(right < n)
{
//判断进窗口
if(nums[right] == 0)
zero++;
right++;
//判断出窗口
while(zero > k)
{
if(nums[left] == 0)
zero--;
++left;
}
//更新结果
len = max(len,right - left);
}
if(k >= n)
return n;
return len;
}
};
四、将 x 减到 0 的最小操作数
1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)
思路:
//只能移动两边
//可以转化为在中间寻找最长子数组,
//该数组的值正好为nums的和-x
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
//只能移动两边
//可以转化为在中间寻找最长子数组,
//该数组的值正好为nums的和-x
int len=-1, left=0, right=0;
int sum=0, tmp=0, n=nums.size();
for(auto e : nums)
sum += e;
if(x > sum)
return -1;
sum -= x;
while(right < n)
{
//进窗口
tmp += nums[right];
++right;
//判断出窗口
if(tmp > sum)
{
while(tmp > sum)
{
tmp -= nums[left];
++left;
}
}
//更新结果
if(tmp == sum)
len = max(len, right - left);
}
if(len == -1)
return -1;
return n - len;
}
};
五、水果成篮
思路:
//转化为求最大子数组
//该数组最多不超过2个元素
//使用滑动窗口
class Solution {
public:
int totalFruit(vector<int>& fruits)
{
//转化为求最大子数组
//该数组最多不超过2个元素
//使用滑动窗口
int left=0,right=0,len=0,n = fruits.size();
unordered_map<int,int> hash;
while(right < n)
{
//进窗口
hash[fruits[right]]++;
right++;
//出窗口
while(hash.size() > 2)
{
hash[fruits[left]]--;
if(hash[fruits[left]] == 0)
{
hash.erase(fruits[left]);
}
++left;
}
//更新结果
len = max(len, right - left);
}
return len;
}
};
六、找到字符串中所有字母异位词
LCR 015. 找到字符串中所有字母异位词 - 力扣(LeetCode)
思路:
//转化为寻找固定长度子数组匹配问题
//哈希表记录子串用于比较
//用滑动窗口
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
//转化为寻找固定长度子数组匹配问题
//哈希表记录子串
//用滑动窗口
int n = p.size();
int left=0, right=0;
unordered_map<char,int> hash1;
unordered_map<char,int> hash2;
vector<int> ret;
for(auto e : p)
{
hash1[e]++;
}
while(right < s.size())
{
//进窗口
hash2[s[right]]++;
++right;
//出窗口
if(right - left > n)
{
hash2[s[left]]--;
if(hash2[s[left]] == 0)
hash2.erase(s[left]);
++left;
}
//更新结果
if(hash1 == hash2)
ret.push_back(left);
}
return ret;
}
};
七、串联所有单词的子串
思路:这一题,有一点难度,但是,也只是把单词匹配换成了字符串匹配问题。单词的比较比较简单,但是字符串的比较不太容易。因此,我们转变思路,设置一个计数器,当插入一个单词时,查看哈希表,看是否存在,有就count++,其余不管。当时count计数与单词个数相同时间,说明匹配,记录相关位置。这是一个比较有意思的题,可以好好的琢磨。问题转化的能力,容器的使用,代码运行过程细节处理,很考验人的。
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
int n = words[0].size(), m = words.size();
vector<int> ret;
unordered_map<string, int> hash1;
//
for(auto& e : words)
hash1[e]++;
for(int i = 0; i<n; ++i)
{
int left = i;
int right = i;
int count = 0;
unordered_map<string, int> hash2;
while(right < s.size())
{
//进窗口
string in = s.substr(right, n);
hash2[in]++;
if(hash2[in] <= hash1[in])//小于等于,表示hash2[in]是hash1[in]的一部分
count++;
//判断
if(right - left + 1 > n * m)
{
string out = s.substr(left, n);
if(hash2[out] <= hash1[out])
count--;
hash2[out]--;
left += n;
}
//检查更新
if(count == m)
ret.push_back(left);
right += n;
}
}
return ret;
}
};