滑动窗口
209. 长度最小的子数组
- 核心思路
滑动窗口的本质是「用两个指针维护一个动态的区间」,通过调整窗口的左右边界,找到满足条件的最小窗口:
- 初始化 :左指针
left=0,当前窗口和sum=0,最小长度minLen=Integer.MAX_VALUE; - 扩展右边界 :右指针
right遍历数组,将nums[right]加入窗口和sum; - 收缩左边界 :当
sum ≥ target时,尝试收缩左边界以找到更小的窗口:
-
- 计算当前窗口长度(
right - left + 1),更新minLen; - 从
sum中减去nums[left],左指针left++,直到sum < target;
- 计算当前窗口长度(
- 结果处理 :若
minLen未更新(仍为最大值),返回0,否则返回minLen。
java
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int l=0,r=0,n=nums.length;
int sum=0,minLen=Integer.MAX_VALUE;
while(r<n){
sum+=nums[r]; //积累值
//大于target值
while(sum>=target){
//更新最短
minLen=Math.min(r-l+1,minLen);
sum-=nums[l];
l++;
}
r++;
}
return minLen==Integer.MAX_VALUE?0:minLen;
}
}
3. 无重复字符的最长子串
最优解法:滑动窗口 + 数组判重(O (n) 时间 + O (1) 空间)
- 核心思路
滑动窗口维护「当前无重复字符的子串区间」,结合数组记录字符最后出现的下标,快速判断字符是否重复并调整窗口左边界:
- 初始化:
-
- 左指针
l=0(窗口左边界),记录最长长度maxLen=0; - 用长度为 128 的数组
a(覆盖 ASCII 所有字符),存储每个字符最后出现的下标,初始值为-1(表示未出现)。
- 左指针
- 扩展右边界 :右指针
r遍历字符串每个字符s.charAt(r); - 判重并调整左边界:
-
- 若当前字符最后出现的下标
a[c] ≥ left(说明字符在当前窗口内重复),则将左指针移至a[c] + 1(跳过重复字符);
- 若当前字符最后出现的下标
- 更新状态:
-
- 记录当前字符的最新下标
a[c] = r; - 计算当前窗口长度
r - l + 1,更新maxLen;
- 记录当前字符的最新下标
- 返回结果 :遍历结束后返回
maxLen。
java
class Solution {
public int lengthOfLongestSubstring(String s) {
//存字符出现的下标
int[] a=new int[128];
Arrays.fill(a,-1); //赋值-1
int n=s.length();
if(n==0||n==1) return n;
int l=0,r=0,maxLen=Integer.MIN_VALUE;
while(r<n){
int c=(int)s.charAt(r);
//出现重复的字符:跳到下一个出现的位置(前面的都统计过了,所以不要l=l+1)
if(a[c]>=l){
l=a[c]+1;
}
a[c]=r; //更新最新坐标
maxLen=Math.max(r-l+1,maxLen); ///更新
r++;
}
return maxLen;
}
}
1004. 最大连续1的个数 III
核心逻辑 :用滑动窗口维护「最多含 k 个 0」的区间,通过调整左右边界找到最大窗口;
- 核心动作
- 预处理优化:若数组长度 ≤ k(可翻转所有 0),直接返回数组长度(所有元素可变为连续 1);
- 滑动窗口遍历:
- 右指针
r遍历数组,统计窗口内 0 的数量cnt; - 当窗口内 0 的数量超过 k 时,收缩左指针
l,直到cnt ≤ k(保证窗口「最多包含 k 个 0」); - 每次遍历计算当前有效窗口长度,更新最长长度
maxLen;
- 返回结果:遍历结束后返回最长有效窗口长度。
- 本质
这是滑动窗口解决「最多允许 k 次替换 / 翻转」的连续子数组问题 的核心逻辑:
- 窗口的「有效性定义」:窗口内 0 的数量 ≤ k(最多翻转 k 个 0 为 1,使窗口内全为连续 1);
- 滑动窗口的核心是「扩展右边界探索更大窗口,超出限制时收缩左边界维持有效性」,最终找到满足条件的最大窗口长度(即最长连续 1 的长度)。
java
class Solution {
public int longestOnes(int[] nums, int k) {
int l=0,r=0,n=nums.length;
if(n<=k) return n;
int maxLen=Integer.MIN_VALUE;
int cnt=0; //统计窗口内0的个数
while(r<n){
if(nums[r]==0) cnt++;
//保证窗口内0的个数不超过k个
//l移动到:窗口第一个0的位置下一个位置
while(cnt>k){
if(nums[l]==0){
cnt--;
};
l++;
}
maxLen=Math.max(maxLen,r-l+1);
r++;
}
return maxLen;
}
}
1658. 将 x 减到 0 的最小操作数
核心转化:直接想"从两端拿数字"很绕
- 「移除左右元素和为 x」等价于「数组中存在一个连续子数组 ,其和 = 总和 - x」,且该子数组长度最长(因为总长度 - 最长子数组长度 = 最少移除次数);
就是说
- 总数字和-凑够x的数字和=剩下的数字和
- 要让「拿的数字最少」就要让「剩下的数字最多」
java
class Solution {
public int minOperations(int[] nums, int x) {
//左/右减一个转化为 在中间找一个窗口
//使得 该窗口尽能大->使得两边窗口小
//和为 sum-x
int n=nums.length;
int l=0,r=0,sum=0;
for(int i=0;i<n;i++) sum+=nums[i];
if(sum<x) return -1;
int cnt=sum-x; //需要凑的值
int curSum=0,maxLen=-1;
while(r<n){
curSum+=nums[r]; //计算和
//超过和
while(cnt<curSum){
curSum-=nums[l];
l++;
}
//等于才会更新
if(curSum==cnt) maxLen=Math.max(maxLen,r-l+1);
r++;
}
return maxLen==-1?-1:n-maxLen;
}
}
904. 水果成篮
-
你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
-
你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
-
一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
本质:
- 问题转化 :将「两个篮子装水果 」转化为「找最多含两种元素的最长连续子数组」,是解题核心;
- 滑动窗口逻辑:扩展右边界探索更大窗口,种类超 2 时收缩左边界维持有效性,全程统计窗口最大长度;
- 哈希表作用:高效统计窗口内元素种类及数量,保证「种类数 > 2」的判断准确。
java
class Solution {
public int totalFruit(int[] fruits) {
int n=fruits.length;
int l=0,r=0;
// 水果种类,数量
Map<Integer,Integer> hash=new HashMap<>();
int maxLen=Integer.MIN_VALUE;
while(r<n){
//没有,就设置1
hash.put(fruits[r],hash.getOrDefault(fruits[r],0)+1);
//窗口内元素种类超过2个
while(hash.size()>2){
int fruit=fruits[l];
//水果数量减1
hash.put(fruit,hash.get(fruit)-1);
//没有水果了
if(hash.get(fruit)==0) hash.remove(fruit);
l++;
}
maxLen=Math.max(maxLen,r-l+1);
r++;
}
return maxLen;
}
}
438. 找到字符串中所有字母异位词
核心思路总结
- 核心问题转化
将「寻找字符串 s 中 p 的所有异位词起始下标」转化为「寻找 s 中长度等于 p、字符频次与 p 完全匹配的连续子串」,异位词的本质是字符种类和频次完全相同、长度一致。
s的长度都小于的长度,不能是
移动左指针的关键
-
窗口内出现了p中没有的字符,也就是cnt[cur]<0,必须移动
-
窗口超过p的长度 ,必须移动左指针
-
- 不管有没有符合字母异位词(如果符合,可以加起始索引啦)
- 核心实现步骤
(1)预处理与边界防护
- 用长度为 26 的数组
cnt统计字符串 p 中每个小写字母的出现频次(cnt[字符-'a']记录对应字符频次); - 若 s 的长度小于 p 的长度,直接返回空列表(不可能存在异位词)。
(2)滑动窗口遍历(双指针维护有效窗口)
-
扩展右边界 :右指针
r遍历 s,将当前字符的频次在cnt中减 1; -
收缩左边界(频次异常) :若当前字符频次变为负数(说明该字符不在 p 中 / 窗口内该字符频次超过 p 的频次),持续右移左指针
l,并恢复左指针对应字符的频次,直到当前字符频次≥0; -
校验窗口有效性 :当窗口长度(
r-l+1)等于 p 的长度时,判断cnt数组是否全为 0(全 0 表示窗口内字符频次与 p 完全匹配): -
- 若匹配,记录当前左指针
l(异位词起始下标); - 无论是否匹配,都需右移左指针
l并恢复对应字符的频次,继续寻找下一个可能的有效窗口。
- 若匹配,记录当前左指针
- 核心判断逻辑
- 用
isAllEmpty方法判断cnt数组是否全为 0,以此验证窗口内字符频次是否与 p 完全一致; - 滑动窗口的核心是「先保证窗口内字符频次不超限(无负数),再校验窗口长度匹配,最终验证频次全匹配」。
java
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> ret=new ArrayList<>();
int[] cnt=new int[26];
int l=0,r=0,n=s.length(),pLen=p.length();
//长度不足,根本找不到
if(n<pLen) return ret;
for(int i=0;i<pLen;i++) cnt[p.charAt(i)-'a']++;
while(r<n){
int cur=s.charAt(r)-'a';
cnt[cur]--;
//移动左指针:出现负数(窗口内出现p没有的字符)
while(cnt[cur]<0){
cnt[s.charAt(l)-'a']++;
l++;
}
//移动左指针:符合窗口大小
if(r-l+1==pLen){
//窗口内出现字符次数=p的字符出现个数
if(isAllEmpty(cnt)) ret.add(l);
//也要移动左指针
cnt[s.charAt(l)-'a']++;
l++;
};
r++;
}
return ret;
}
//判断p的字符个数和窗口内字符的个数一样
public boolean isAllEmpty(int[] cnt){
for(int i=0;i<26;i++){
if(cnt[i]!=0) return false;
}
return true;
}
}