十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

滑动窗口算法:从暴力解法到高效优化的实战指南

滑动窗口算法:从暴力解法到高效优化的实战指南 1. 滑动窗口算法入门从暴力解法到高效优化第一次接触滑动窗口是在LeetCode第209题长度最小的子数组当时我用了最直接的暴力解法——双重循环枚举所有可能的子数组。虽然通过了测试用例但面对大数据量时直接超时。这种O(n²)的时间复杂度让我开始思考更优解直到发现了滑动窗口这个神奇的思想。滑动窗口本质上是一种双指针技巧的变体它通过维护一个动态变化的窗口来减少不必要的计算。想象你在公交车上观察窗外风景窗口大小固定随着车辆前进旧风景离开视野新风景进入视野。算法中的滑动窗口也是如此只不过我们观察的是数组或字符串的子区间。提示滑动窗口特别适合解决连续子数组/子串类问题尤其是涉及最大值、最小值、平均值或特定条件的题目。2. 滑动窗口的两种基本类型2.1 固定大小的滑动窗口这类问题的窗口大小在解题过程中保持不变典型例题是LeetCode 239滑动窗口最大值。我第一次尝试时直接用了暴力法def maxSlidingWindow(nums, k): return [max(nums[i:ik]) for i in range(len(nums)-k1)]虽然简洁但时间复杂度是O(nk)当n和k都很大时性能堪忧。后来学习到可以用单调队列优化到O(n)from collections import deque def maxSlidingWindow(nums, k): q deque() res [] for i, num in enumerate(nums): while q and nums[q[-1]] num: q.pop() q.append(i) if q[0] i - k: q.popleft() if i k - 1: res.append(nums[q[0]]) return res2.2 可变大小的滑动窗口更常见的是窗口大小可变的场景如LeetCode 3无重复字符的最长子串。这类问题通常需要维护某些条件如字符唯一性通过左右指针的移动来寻找最优解。我的解题模板如下def lengthOfLongestSubstring(s): char_set set() left 0 max_len 0 for right in range(len(s)): while s[right] in char_set: char_set.remove(s[left]) left 1 char_set.add(s[right]) max_len max(max_len, right - left 1) return max_len3. 滑动窗口解题的通用模板经过上百道题的实践我总结出了滑动窗口的通用解题框架初始化阶段定义左右指针通常leftright0创建哈希表或变量记录窗口状态初始化结果变量窗口扩张阶段移动右指针扩展窗口更新窗口状态如字符计数、和值等条件判断阶段检查当前窗口是否满足条件如果满足更新结果如果不满足进入收缩阶段窗口收缩阶段移动左指针收缩窗口更新窗口状态返回条件判断阶段以LeetCode 76最小覆盖子串为例的代码实现def minWindow(s, t): from collections import defaultdict target defaultdict(int) for c in t: target[c] 1 left 0 count len(t) min_len float(inf) result for right in range(len(s)): if target[s[right]] 0: count - 1 target[s[right]] - 1 while count 0: if right - left 1 min_len: min_len right - left 1 result s[left:right1] target[s[left]] 1 if target[s[left]] 0: count 1 left 1 return result4. 滑动窗口的常见变种与解题技巧4.1 多指针滑动窗口有些问题需要维护多个指针如LeetCode 930和相同的二元子数组。这类题目通常需要记录前缀和或使用哈希表辅助def numSubarraysWithSum(nums, goal): from collections import defaultdict prefix defaultdict(int) prefix[0] 1 res 0 curr_sum 0 for num in nums: curr_sum num res prefix[curr_sum - goal] prefix[curr_sum] 1 return res4.2 带计数的滑动窗口如LeetCode 904水果成篮需要维护窗口内元素的种类数def totalFruit(fruits): from collections import defaultdict basket defaultdict(int) left 0 max_fruits 0 for right, fruit in enumerate(fruits): basket[fruit] 1 while len(basket) 2: basket[fruits[left]] - 1 if basket[fruits[left]] 0: del basket[fruits[left]] left 1 max_fruits max(max_fruits, right - left 1) return max_fruits4.3 滑动窗口与单调栈的结合某些问题需要结合单调性来优化如LeetCode 1438绝对差不超过限制的最长连续子数组def longestSubarray(nums, limit): from collections import deque max_q deque() min_q deque() left 0 res 0 for right, num in enumerate(nums): while max_q and num max_q[-1]: max_q.pop() max_q.append(num) while min_q and num min_q[-1]: min_q.pop() min_q.append(num) while max_q[0] - min_q[0] limit: if nums[left] max_q[0]: max_q.popleft() if nums[left] min_q[0]: min_q.popleft() left 1 res max(res, right - left 1) return res5. 滑动窗口常见错误与调试技巧5.1 边界条件处理新手常犯的错误包括窗口初始化不正确特别是right从0还是1开始结果更新时机错误应该在收缩前还是收缩后忘记处理空输入或特殊输入注意在每次写滑动窗口代码时务必手动跑这几个测试用例空输入如空字符串或空数组单元素输入所有元素都相同的输入刚好满足条件的边界情况5.2 窗口收缩条件收缩条件过于宽松或严格都会导致错误。我的调试方法是在循环中加入打印语句输出窗口状态画图模拟窗口移动过程对简单测试用例手动计算预期结果5.3 性能优化当遇到超时问题时检查是否有不必要的重复计算可以用哈希表缓存窗口收缩是否可以更积极尽早移动左指针数据结构选择是否最优如用数组代替哈希表6. 滑动窗口在面试中的实战应用在技术面试中滑动窗口问题出现的频率极高。根据我的面试经验面试官通常期待快速识别问题类型能否在1-2分钟内判断出可以使用滑动窗口代码实现能力在15-20分钟内写出无bug的代码复杂度分析准确分析时间空间复杂度边界处理考虑各种极端情况优化思路能否提出进一步优化的方向我建议按照这个流程应对滑动窗口面试题明确问题要求连续子数组/子串、求最大/最小值等举例说明暴力解法及其复杂度提出滑动窗口优化思路讨论窗口移动条件和状态维护方式编写代码并测试边界条件分析复杂度并讨论可能的优化7. 滑动窗口进阶处理更复杂的问题7.1 多维滑动窗口有些问题需要在二维矩阵上应用滑动窗口如LeetCode 1074元素和为目标值的子矩阵数量。这类问题通常需要固定行或列的维度在另一个维度上应用滑动窗口结合前缀和优化计算def numSubmatrixSumTarget(matrix, target): rows, cols len(matrix), len(matrix[0]) count 0 for i in range(rows): col_sum [0] * cols for j in range(i, rows): prefix {0: 1} curr_sum 0 for k in range(cols): col_sum[k] matrix[j][k] curr_sum col_sum[k] count prefix.get(curr_sum - target, 0) prefix[curr_sum] prefix.get(curr_sum, 0) 1 return count7.2 滑动窗口与动态规划结合某些问题需要结合DP思想如LeetCode 1151最少交换次数来组合所有的1。解题思路使用滑动窗口确定目标窗口用DP计算达到目标所需的最小交换次数def minSwaps(data): ones sum(data) if ones 0: return 0 window sum(data[:ones]) max_ones window for i in range(ones, len(data)): window data[i] - data[i - ones] max_ones max(max_ones, window) return ones - max_ones7.3 时间序列上的滑动窗口处理时间序列数据时滑动窗口也非常有用。例如LeetCode 1838最高频元素的频数需要考虑元素的增量操作def maxFrequency(nums, k): nums.sort() left 0 total 0 max_freq 1 for right in range(1, len(nums)): total (nums[right] - nums[right-1]) * (right - left) while total k: total - nums[right] - nums[left] left 1 max_freq max(max_freq, right - left 1) return max_freq8. 滑动窗口算法的时间复杂度分析正确分析滑动窗口算法的时间复杂度是面试中的关键点。一般来说基础滑动窗口O(n)每个元素最多被左右指针各访问一次如LeetCode 3无重复字符的最长子串带哈希表的滑动窗口O(n)哈希操作平均O(1)如LeetCode 76最小覆盖子串带单调队列的滑动窗口O(n)每个元素入队出队各一次如LeetCode 239滑动窗口最大值嵌套循环的滑动窗口O(nk)内层循环可能执行k次需要特殊优化才能降为O(n)空间复杂度通常为O(k)或O(n)取决于需要维护的辅助数据结构。在实际分析时我会特别注意最坏情况下哈希表可能达到O(n)空间单调队列的空间通常是O(k)如果只使用有限变量空间可以是O(1)9. 滑动窗口与其他算法的对比9.1 滑动窗口 vs 双指针虽然滑动窗口是双指针的一种应用但两者有区别双指针通常用于有序数组指针移动有特定规律滑动窗口关注子区间性质指针移动由窗口条件决定9.2 滑动窗口 vs 动态规划选择依据滑动窗口连续子数组/子串的最值问题动态规划非连续子序列或需要记忆化的问题有时可以结合使用如先用滑动窗口预处理再用DP求解。9.3 滑动窗口 vs 前缀和前缀和适合需要频繁计算任意区间和问题可以转化为寻找特定和值滑动窗口适合需要维护窗口内的特定性质窗口大小固定或可变但连续10. 滑动窗口实战训练建议根据我的刷题经验建议按这个顺序练习基础阶段掌握模板长度最小的子数组无重复字符的最长子串最小覆盖子串进阶阶段理解变种水果成篮和相同的二元子数组最大连续1的个数 III高手阶段综合应用滑动窗口最大值绝对差不超过限制的最长连续子数组最高频元素的频数我的训练方法是第一遍自己思考并实现第二遍学习最优解比较差异第三遍一周后重做检验掌握程度建立错题本记录典型错误和优化思路对于想系统掌握滑动窗口的同学我建议至少完成30道相关题目覆盖各种变种和难度级别。在实际编码时养成先写注释再填充代码的习惯明确每个步骤的意图这样可以大大减少调试时间。
返回列表