1. 滑动窗口算法精讲:从入门到精通
作为一名算法爱好者,我深知滑动窗口算法在解决子数组/子字符串问题中的重要性。今天我想和大家分享两道LeetCode经典题目(713和1358)的详细解析,希望能帮助初学者更好地掌握这一技巧。
滑动窗口算法本质上是一种双指针技巧,特别适合处理数组/字符串中的连续子序列问题。它的核心思想是维护一个窗口,通过调整窗口的左右边界来高效地解决问题,通常能将O(n²)的时间复杂度优化到O(n)。
2. 713题:乘积小于K的子数组解析
2.1 问题重述与理解
题目要求我们统计数组中所有乘积严格小于k的连续子数组的个数。以示例1为例:
- 输入:nums = [10,5,2,6], k = 100
- 输出:8
- 解释:符合条件的子数组有[10],[5],[2],[6],[10,5],[5,2],[2,6],[5,2,6]
关键点在于:
- 子数组必须是连续的
- 所有元素的乘积必须严格小于k
- 需要统计所有可能的子数组数量
2.2 算法思路详解
滑动窗口解法步骤如下:
- 初始化左指针left=0,结果ans=0,乘积mul=1
- 遍历数组,右指针i从0到n-1:
a. 将当前元素乘入mul
b. 当mul≥k时,从左开始除nums[left]并右移left
c. 每次循环结束时,ans增加i-left+1
核心逻辑在于:对于每个右边界i,以i结尾的符合条件的子数组数量就是窗口长度(i-left+1)
2.3 代码实现与逐行解析
cpp复制class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
if (k <= 1) return 0; // 特殊情况处理
int left = 0, ans = 0, mul = 1;
for (int i = 0; i < nums.size(); i++) {
mul *= nums[i]; // 扩展右边界
while (mul >= k) { // 收缩左边界
mul /= nums[left];
left++;
}
ans += i - left + 1; // 关键步骤
}
return ans;
}
};
2.4 复杂度分析与优化
时间复杂度:O(n),每个元素最多被访问两次(被乘入和被除出)
空间复杂度:O(1),只使用了常数个额外空间
注意事项:当k≤1时直接返回0,因为nums元素都是正整数,乘积不可能小于等于0或1
3. 1358题:包含所有三种字符的子字符串数目
3.1 问题理解与示例分析
题目要求统计包含至少一个a、b、c的子字符串数量。示例1:
- 输入:s = "abcabc"
- 输出:10
- 解释:符合条件的子串有"abc","abca","abcab","abcabc","bca","bcab","bcabc","cab","cabc","abc"
关键点:
- 子字符串必须包含a、b、c各至少一个
- 相同子串在不同位置出现要分别计数
- 只需要包含至少一个,可以有多个
3.2 滑动窗口解法设计
算法思路:
- 使用cnt数组统计窗口内a、b、c的出现次数
- 移动右边界,增加当前字符计数
- 当三种字符都出现时,计算符合条件的子串数
- 移动左边界直到不满足条件
3.3 代码实现与关键点解释
cpp复制class Solution {
public:
int numberOfSubstrings(string s) {
int left = 0, ans = 0;
int cnt[3] = {0}; // 统计a,b,c出现次数
for (int i = 0; i < s.size(); i++) {
cnt[s[i]-'a']++; // 更新当前字符计数
while (cnt[0] && cnt[1] && cnt[2]) { // 满足条件时
cnt[s[left]-'a']--; // 移动左边界
left++;
}
ans += left; // 关键步骤
}
return ans;
}
};
3.4 为什么ans += left?
这个逻辑可能比较难理解,让我们通过示例分解:
以s="abcabc"为例:
- i=0 (a): cnt=[1,0,0], left=0, ans+=0 (ans=0)
- i=1 (b): cnt=[1,1,0], left=0, ans+=0 (ans=0)
- i=2 (c): cnt=[1,1,1]
- 进入while: cnt[a]--, left=1
- ans+=1 (ans=1) → 对应"abc"
- i=3 (a): cnt=[1,1,1]
- while: cnt[b]--, left=2
- ans+=2 (ans=3) → 对应"bca"和"abca"
- i=4 (b): cnt=[1,1,1]
- while: cnt[c]--, left=3
- ans+=3 (ans=6) → 对应"cab","bcab","abcab"
- i=5 (c): cnt=[1,1,1]
- while: cnt[a]--, left=4
- ans+=4 (ans=10) → 对应"abc","cabc","bcabc","abcabc"
4. 滑动窗口算法通用模板
通过这两道题,我们可以总结出滑动窗口的通用模板:
cpp复制int slidingWindowTemplate(vector<int>& nums, int k) {
int left = 0, result = 0;
// 可能需要初始化其他变量
for (int right = 0; right < nums.size(); right++) {
// 更新窗口状态(如累加、计数等)
while (/* 不满足条件 */) {
// 移动左边界并更新状态
left++;
}
// 更新结果(可能在while循环外或内)
result += ...;
}
return result;
}
4.1 滑动窗口适用场景
- 连续子数组/子字符串问题
- 需要统计或查找满足特定条件的子序列
- 通常涉及"最小"、"最大"、"数量"等关键词
4.2 常见变种与解题技巧
- 固定窗口大小:如求长度为k的子数组的最大和
- 可变窗口大小:如这两道题
- 需要辅助数据结构:如哈希表记录字符出现位置
- 可能需要预处理:如前缀和等
5. 算法学习心得与建议
在解决这两道题的过程中,我总结了以下几点经验:
- 理解题意是关键:明确什么是子数组/子字符串,条件是什么
- 画图辅助理解:特别是窗口移动的过程
- 从暴力法思考:先想O(n²)解法,再优化为滑动窗口
- 注意边界条件:如k=0或字符串长度不足的情况
- 调试时打印中间变量:如cnt数组和left、right指针位置
对于算法初学者,我的建议是:
- 先掌握基础数据结构:数组、字符串、哈希表等
- 理解时间复杂度的概念
- 从简单题开始,逐步提升难度
- 坚持每日一题,保持手感
- 多写解题报告,加深理解
滑动窗口算法看似简单,但要灵活运用需要大量练习。我建议可以按照以下顺序练习:
-
- 长度最小的子数组
-
- 无重复字符的最长子串
-
- 最小覆盖子串
-
- 替换后的最长重复字符
-
- 和相同的二元子数组
记住,算法学习是一个循序渐进的过程,不要因为一时不理解而气馁。多思考、多练习、多总结,你一定能掌握滑动窗口这一强大技巧。