1、长度最小的子数组
209. 长度最小的子数组 - 力扣(LeetCode)
https://leetcode.cn/problems/minimum-size-subarray-sum/description/
利用单调性,使用"同向双指针"来优化
什么是滑动窗口:同向双指针也叫滑动窗口
什么时候用滑动窗口:利用单调性的时候
当使用暴力解法的时候,两个指针不回退,都是向同一个方向移动的时候
怎么用?
先初始化两个指针,让他充当窗口的左端点和右端点
滑动窗口的正确性:利用单调性,规避了很多没有必要的枚举
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int sum = 0, len = INT_MAX;//因为我们要求最小的len,因此不能将len定义为0,我们要定义一个大数
for(int left = 0, right = 0; right < nums.size(); right++)
{
//1、进窗口 -- right所指的位置进入窗口
sum += nums[right];
while(sum >= target)
{
len = min(right - left + 1, len);
sum -= nums[left++];//缩小窗口 因为你已经将left移动了 因此我们要先取长度 再挪动(出窗口)
}
}
return len == INT_MAX ? 0;//最后需要判断一下len等于INT_MAX 就返回0 这是没有找到和大于等于target
}
};
2、无重复字符的最长子串

cpp
class Solution {
public:
int lengthOfLongestSubstring(string s)
{
int hash[128] = {0}; //用数组来模拟哈希表
int left = 0, right = 0, n = s.size();
int ret = 0;
while(right < n)
{
hash[s[right]]++; //进入窗口---给字符个数+1 本来字符个数是0
while(hash[s[right]] > 1) //判断---right从左往右遍历字符,当字符个数大于1时,就代表left和right中间的字符串有重复的字符了,上一次right的位置的字符的长度就是可以保留的,现在就要让left指向的字符出窗口(以left为左边的字符串已找到最大值)
hash[s[left++]]--; //出窗口---为什么出窗口一定要让字符的个数减一? 因为出了之后就不会再算这个字符就会再开始新的一轮的计数 所以必须要减一
ret = max(ret,right - left + 1); //更新结果
right++; //让下一个元素进窗口
}
return ret;
}
};
暴力解法:枚举出所有子字符串。以一个位置为起点,向后枚举,直到遇到有重复的字符。就换下一个起点。直到最后一个字符。 统计所有的长度,找出最大值就好了。(暴力枚举+哈希表(判断字符是否重复出现))
我们对暴力解法进行优化:

为什么能够使用滑动窗口来解决问题:(重要)
left开始在d,right一直向右移动,走到a发现有重复就停止了。deabc就是以d为左边的最多是长度,然后,如果让e为左边,right还是走到a就停止,那么这个长度肯定是小于第一个长度的。因为left后面有a这个字符,所以right肯定是走到a就停了。因此,我们要让left指针跳过a,那么还要让right指针回到b再往后走吗,肯定不用,因为前面没有重复的了,因为right能走到a,说明前面已经没有重复的了,因此只需要将right再向后面移动即可。那么就是left和right指针都向右移动。那么我们就可以使用"滑动窗口"来解决问题。
写滑动窗口的代码逻辑:
1、left = 0, right = 0
2、进窗口 ---- 让字符进入哈希表
3、判断 ---窗口内出现重复字符
出窗口就是从哈希表中删除字符
更新结果
3、最大连续1的个数
暴力解法:枚举子数组,子数组的0的个数=k(固定一边,移动另一边,找到最大的长度。找到最大的长度之后,就换一个新的开头),找出所有的子数组。
我们使用滑动窗口解法:
如何优化为滑动窗口的????

我们right移动到第三个0的时候,窗口就不合法了,我们将left继续向右移动,(暴力枚举是让l移动一位,然后r再从左往右移动)优化之后,l一直向右移动,r不变,直到r和l的区间中0的个数不大于2,这样我们就会发现,l和r是同向移动的,因此,可以使用滑动窗口。
1、定义left和right
2、进窗口:【我们要统计窗口中0的个数】
如果是1,无视
如果是0,计数器加一
3、判断 出窗口
当窗口不合法的时候,我们就让left一直右移,直到这个窗口合法位置(因此,这个过程是一个循环) 判断-不合法-出窗口-判断-不合法-继续出窗口
什么时候更新结果???
当我们是一个合法窗口的时候就更新结果
cpp
class Solution {
public:
int longestOnes(vector<int>& nums, int k)
{
int ret = 0;
for(int left = 0, right = 0, zero = 0; right < nums.size(); right++)
{
if(nums[right] == 0) zero++; //进窗口
//判断
while(zero > k) //判断0的个数是否超过k个 //出窗口
{
if(nums[left] == 0) //指向元素为0
zero--;
left++;
}
ret = max(ret ,right - left +1); //更新结果
}
return ret;
}
};
4、将X减到0的最小操作数
https://leetcode.cn/problems/minimum-operations-to-reduce-x-to-zero/
我们要从左边或者右边取数,让x减到0
我们可以使用"正难则反"的策略
转化:找出最长的子数组的长度,所有元素的和正好等于sum-x
cpp
class Solution {
public:
int minOperations(vector<int>& nums, int x)
{
int sum = 0;
for(int a : nums)//把每个数都取出来
sum += a; //数组中每个数的和
int target = sum - x; //将问题转化为 找出和为sum-x的子数组,并且长度最长 -使用滑动窗口
if(target < 0) return -1;
int ret = -1;
for(int left = 0, right = 0, tmp = 0;right < nums.size(); right++)
{
tmp += nums[right];//进窗口 给tmp加上数字 tmp是窗口内的数字和
while(tmp > target)//和大于target就要出窗口
tmp -= nums[left++];//左边的向右移动出窗口
if(tmp == target) //如果等于了 我们就要更新长度
ret = max(ret, right - left + 1);
}
if(ret == -1) return -1;
else return nums.size() - ret;
}
};
5、水果成篮
https://leetcode.cn/problems/fruit-into-baskets/description/


上面一段文字描述的问题其实是:找出一个最长的子数组的长度,子数组不超过两种类型的水果。
解法一:暴力枚举(找出所有可能的子数组情况,选最长的)+哈希表(把数扔到哈希表里面,看有没有超过两种)
解法二:我们优化暴力枚举,当right已经到了大于两种水果的地方,left就向右移动,left移动的过程中
1)kinds不变(水果种类不变)----right不变
2)kinds变--- right变小
我们就可以直到right是不用再往回走的,因为走回去还是需要走过来,所以right是向右走的,left也是向右走的。
所以,方向相同,就用滑动窗口。
1、left、right指针
2、进窗口----将right所指的元素丢到hash表里面,哈希表里面不仅要存水果的种类还要存水果的数量。
3、判断 --- 哈希表的长度大于2就出窗口
出窗口---left位置所对应的水果种类减减
更新结果
cpp
class Solution
{
public:
int totalFruit(vector<int>& f)
{
unordered_map<int,int> hash;//使用哈希表统计出现了多少次水果
int ret = 0;
for(int left = 0, right = 0; right < f.size(); right++)
{
//进窗口
hash[f[right]]++;
//判断
while(hash.size() > 2) //hash.size返回元素的种类
{
//出窗口
hash[f[left]]--; //先在哈希表里面将这个种类的水果数量减一 hash[1] 就是键对应的值,而不是键
if(hash[f[left]] == 0) //减完要判断值是不是为0,为0了就要将这个水果种类从哈希表中删除
hash.erase(f[left]);//f[left] 是键 通过键删除
left++;
}
ret = max(ret, right - left + 1);
}
return ret;
}
};
6、找到字符串中所有字母的异位词
https://leetcode.cn/problems/find-all-anagrams-in-a-string/

暴力解法:
p字符串放到哈希表1里面再将s字符串中,找到和p字符串长度一样的所有子串,找到一个子串就放到哈希表2中,如果两个哈希表相同,就代表找到了,找到之后将其实位置记录,再继续向后找。
优化暴力解法:
我们不用将cba放进入,再把他们清除,再把bae放进入,因为ba是重复的,我们只需要把c移出去,把e放进来。
这样我们的"窗口"大小就是固定的,left和right都是往同一个方向移动的,因此就可以优化为:
滑动窗口+哈希表
cpp
class Solution
{
public:
vector<int> findAnagrams(string s, string p)
{
int hash1[26] = {0}; //数组来模拟hash表 来统计每个字符出现的次数 每个下标代表一个字符 数组的元素代表该下标对应的字符出现的个数
for(auto ch : p) hash1[ch - 'a']++;//通过ascii统计字符出现的次数 字符-a就是数组下标 0对应的a 1对应的是b
int hash2[26] = {0};//统计窗口中每个字符出现的次数
vector<int> ret;
int m = p.size();
for(int left = 0, right = 0, count = 0; right < s.size(); right++)
{
char in = s[right];
hash2[in -'a']++;//把字符放进哈希表中统计个数 就是 进窗口
if(hash2[in - 'a'] <= hash1[in - 'a']) count++; //两个不同的数组 但是下标是一样的 那么进行比较的时候也是比较的是同一个字符的个数
if(right - left +1 > m)//判断
{
char out = s[left++];//出窗口 先拿到需要出的字符 再把指针向右挪动
if(hash2[out - 'a'] <= hash1[out - 'a']) count--;
hash2[out - 'a']--;
}
if(count == m) ret.push_back(left);
}
return ret;
}
};
1、先移动right 2、再给right对应的字符个数加1(进窗口) 3、再判断字符个数是不是小于等于p字符串中统计出来的字符个数(如果小于等于就是有效字符【相等的情况就是2刚好给字符个数加一就和1中的个数相等了】)
4、是有效字符就给count加一(加到三就说明找到了,就把起始位置放到数组中给存起来)

