今天我想和大家分享一个非常实用的算法技巧——滑动窗口(Sliding Window)。这个技巧在处理字符串和数组相关问题时特别高效,尤其是那些需要统计连续子串或子数组特性的题目。我们以LeetCode第1456题"定长子串中元音的最大数目"为例,深入剖析这个算法的实现细节和应用场景。
滑动窗口算法的核心思想是维护一个大小固定的窗口,在数据序列上滑动,通过局部信息的增量更新来避免重复计算。相比暴力解法,它能将时间复杂度从O(n²)降低到O(n),在处理大规模数据时优势尤为明显。
给定一个字符串s和一个整数k,要求找到所有长度为k的子串中,包含元音字母(a, e, i, o, u)的最大数量。例如:
最直观的解法是枚举所有长度为k的子串,分别统计每个子串中的元音数量,然后取最大值。这种方法的时间复杂度是O(nk),当n和k较大时(比如n=10^5,k=10^4),计算量会达到10^9次操作,这在编程竞赛或面试中显然是不可接受的。
滑动窗口算法通过维护一个固定大小的窗口,利用前一个窗口的计算结果来优化当前窗口的计算。具体到这个问题:
cpp复制class Solution {
public:
int maxVowels(string s, int k) {
int ans = 0;
int left = 0;
int right = 0;
int curNum = 0;
unordered_set<char> vowels = {'a', 'e', 'i', 'o', 'u'};
// 初始化第一个窗口
for(right; right < k; right++) {
if(vowels.count(s[right])) {
curNum++;
}
}
right--; // 调整right到窗口右边界
ans = curNum;
// 滑动窗口
left++;
right++;
for(; right < s.size(); left++, right++) {
// 处理离开窗口的字符
if(vowels.count(s[left - 1])) {
curNum--;
}
// 处理进入窗口的字符
if(vowels.count(s[right])) {
curNum++;
}
ans = max(ans, curNum);
}
return ans;
}
};
灵茶山艾府老师提出的"入-更新-出"模板更加通用,适用于各种定长滑动窗口问题:
cpp复制class Solution {
public:
int maxVowels(string s, int k) {
int cnt = 0;
int ans = 0;
int i = 0;
int j = 0;
while(j < s.size()) {
// 入:新元素进入窗口
if(s[j] == 'a' || s[j] == 'e' || s[j] == 'i' ||
s[j] == 'o' || s[j] == 'u') {
cnt++;
}
// 更新:当窗口大小达到k时
if(j - i + 1 == k) {
ans = max(ans, cnt);
// 出:最左元素离开窗口
if(s[i] == 'a' || s[i] == 'e' || s[i] == 'i' ||
s[i] == 'o' || s[i] == 'u') {
cnt--;
}
i++; // 移动左边界
}
j++; // 移动右边界
}
return ans;
}
};
提示:这个模板适用于所有定长滑动窗口问题,只需根据具体问题调整"入"和"出"时的统计逻辑。
两种实现方式的时间复杂度都是O(n),其中n是字符串长度。因为每个字符最多被处理两次(进入窗口和离开窗口各一次)。
空间复杂度都是O(1),只使用了固定数量的变量,与输入规模无关。
窗口大小错误:
边界条件处理不当:
元音计数错误:
打印窗口状态:在每次窗口滑动时打印left、right和当前元音数量
cpp复制cout << "Window [" << left << "," << right << "]: " << curNum << endl;
单元测试用例:
cpp复制// 测试用例1:常规情况
assert(maxVowels("abciiidef", 3) == 3);
// 测试用例2:全元音
assert(maxVowels("aeiou", 2) == 2);
// 测试用例3:无元音
assert(maxVowels("xyz", 1) == 0);
// 测试用例4:k等于字符串长度
assert(maxVowels("hello", 5) == 2);
可视化调试:在纸上画出窗口滑动过程,手动验证算法步骤
掌握了这个定长滑动窗口模板后,我们可以解决许多类似问题:
这些题目都可以套用"入-更新-出"的三步模板,只需根据具体问题调整统计逻辑。
在实际编码和面试中,我有几点经验分享:
滑动窗口算法看似简单,但在实际应用中很容易出现off-by-one错误。我在最初学习时经常因为边界处理不当而调试很久。后来发现,在纸上画出窗口滑动过程并手动模拟几次,能大大减少这类错误。