滑动窗口算法精讲:从LeetCode 209/713/3题掌握C++实现与工程应用 📅 2026/7/13 9:42:22 1. 项目概述滑动窗口算法的核心价值在算法面试和日常开发中处理数组或字符串的子区间问题是一个高频考点。当你面对“连续子数组”、“子串”、“最长/最短”这类关键词时一个强大而优雅的工具——滑动窗口Sliding Window——就该登场了。今天我们就以 LeetCode 上三道经典的题目209、713、3为蓝本深入拆解滑动窗口在 C 中的实现精髓。这不是一篇简单的题解罗列而是我结合多年刷题和面试官经验为你梳理出的一套从理解到精通的心法。无论你是正在备战面试还是希望提升解决实际数据流问题的能力掌握滑动窗口都能让你在面对一大类问题时思路清晰代码简洁。滑动窗口的本质是在线性数据结构如数组、字符串上维护一个动态的区间。这个区间像一扇窗户在数据上滑动我们只关心窗口内的信息通过调整窗口的左右边界来高效地寻找满足特定条件的解。它之所以强大是因为它将许多看似需要 O(n²) 暴力枚举的问题优化到了 O(n) 的时间复杂度。接下来我们将通过三道由浅入深的题目彻底吃透它的两种核心形态可变窗口与固定窗口。2. 核心思路拆解两种窗口一套心法滑动窗口算法主要分为两种类型可变长度窗口和固定长度窗口。理解它们的适用场景和代码模板是灵活运用的关键。2.1 可变长度滑动窗口可变长度窗口通常用于求解“满足条件的最长/最短子数组”问题。窗口的大小不是预先设定的而是根据窗口内元素的性质动态扩张或收缩。其核心思想是扩张右指针right向右移动扩大窗口直到窗口内的状态满足或不满足题目条件。收缩一旦窗口状态触发某个条件例如满足了要求或违反了约束左指针left开始向右移动收缩窗口同时更新状态直到窗口再次变得“合法”。记录答案在窗口滑动的整个过程中在合适的时机通常是在收缩窗口前或后记录当前窗口的信息作为候选答案。这个“扩张-收缩”的循环确保了每个元素最多被右指针和左指针各访问一次时间复杂度为 O(n)。LeetCode 209 和 3 是这类问题的典型代表。2.2 固定长度滑动窗口固定长度窗口则用于处理窗口大小已知的问题例如“长度为 k 的子数组的最大平均值”。它的滑动更为规律初始化先构建第一个长度为 k 的窗口并计算其初始状态如和、乘积、字符统计等。滑动窗口每次向右滑动一格。右指针引入一个新元素左指针移出一个旧元素。我们只需要在旧窗口状态的基础上进行“加新减旧”的增量更新即可得到新窗口的状态无需重新计算整个窗口。更新答案在每次滑动后根据新窗口的状态更新最终答案。这种方法避免了重复计算将复杂度从 O(n*k) 降为 O(n)。LeetCode 713 是这种模式的变体窗口大小可变但核心操作依然是增量更新。注意选择哪种窗口关键在于问题描述。问“最长/最短”且条件是关于“和”、“不同字符数”等累积量的多用可变窗口。问“所有长度为K的子数组是否...”或需要计算每个固定窗口属性的多用固定窗口。3. 实战精讲三道题目的逐行解析理论说得再多不如代码来得实在。我们直接进入这三道经典题目我会用详细的注释和现场推演带你感受滑动窗口的代码之美。3.1 LeetCode 209. 长度最小的子数组题目核心给定一个正整数数组nums和一个正整数target找出数组中满足其和≥ target的长度最小的连续子数组。如果不存在返回 0。思路分析这是可变长度窗口的入门题。我们要找和至少为target的最短区间。窗口状态就是窗口内元素的和sum。扩张右指针移动sum增加目标是让sum target。收缩一旦sum target我们就记录当前窗口长度并尝试收缩左边界左指针右移让sum减小以寻找更短的、同样满足条件的窗口。答案在收缩过程中持续更新最小长度。C 实现与逐行解读class Solution { public: int minSubArrayLen(int target, vectorint nums) { int n nums.size(); int left 0; // 窗口左边界 int sum 0; // 窗口内元素的和 int minLen INT_MAX; // 初始化最小长度为最大整数 // 右指针 right 负责遍历数组扩张窗口 for (int right 0; right n; right) { sum nums[right]; // 将右指针指向的元素加入窗口更新和 // 关键循环当窗口和已经满足条件时尝试收缩窗口以找到更优解 while (sum target) { // 更新最小长度。当前窗口长度为 right - left 1 minLen min(minLen, right - left 1); // 收缩窗口左指针右移将左边元素移出窗口 sum - nums[left]; left; } // 如果 sum target则继续右移 right 扩张窗口 } // 如果 minLen 没有被更新过说明没有符合条件的子数组返回 0 return minLen INT_MAX ? 0 : minLen; } };实操心得minLen初始化为INT_MAX是一个小技巧方便最后判断是否有解。内层的while循环是精髓。它确保了在右指针固定的情况下左指针能一直收缩到窗口不再满足条件为止从而为当前right找到了以它为右端点的、满足条件的最短窗口。时间复杂度是 O(n)因为每个元素最多被加入窗口一次right移动移出窗口一次left移动。空间复杂度是 O(1)。3.2 LeetCode 713. 乘积小于 K 的子数组题目核心给定一个正整数数组nums和一个整数k返回子数组内所有元素的乘积严格小于k的连续子数组的个数。思路分析这道题比 209 更进阶一些。它不再是找最短窗口而是统计所有满足条件的窗口个数。窗口状态是乘积product。由于数组元素都是正数乘积随着窗口扩大而单调递增缩小而单调递减这为滑动窗口创造了条件。扩张右指针移动product乘以nums[right]。收缩当product k时窗口乘积太大了需要收缩左边界左指针右移让product除以nums[left]直到product k。统计答案这是本题的难点。对于每一个固定的右指针right当窗口[left, right]满足乘积小于k时以right为右端点且满足条件的子数组个数是多少答案是right - left 1。为什么呢因为从left到right之间的任何一个位置i作为起点到right结束的子数组[i, right]其乘积一定也小于k正数数组子数组更短乘积更小。所以我们是在窗口满足条件的那一刻进行批量统计。C 实现与逐行解读class Solution { public: int numSubarrayProductLessThanK(vectorint nums, int k) { // 边界条件k 1 时由于数组元素为正整数乘积不可能小于1或0直接返回0。 if (k 1) return 0; int left 0; long long product 1; // 使用 long long 防止乘积溢出虽然题目数据范围可能用int足够 int count 0; // 统计结果 int n nums.size(); for (int right 0; right n; right) { product * nums[right]; // 扩张窗口加入右端元素 // 收缩窗口当乘积过大时移动左指针 while (product k) { product / nums[left]; left; } // 关键统计此时窗口 [left, right] 内所有子数组都满足条件 // 新增的、以 right 为结尾的子数组个数为窗口长度 count (right - left 1); } return count; } };实操心得正数数组是使用滑动窗口的前提保证了乘积的单调性。如果数组包含0或负数此模板不适用。统计时机的理解count (right - left 1)是核心。一定要理解这是在累加以当前right为结尾的所有合法子数组。例如窗口为[2,3,4]则以4结尾的合法子数组有[4],[3,4],[2,3,4]共3个。初始条件判断if (k 1) return 0;非常重要它处理了边界情况避免了无效循环。3.3 LeetCode 3. 无重复字符的最长子串题目核心给定一个字符串s请你找出其中不含有重复字符的最长子串的长度。思路分析这是可变长度滑动窗口最经典的题目之一。窗口状态是窗口内字符的出现情况。我们需要一个数据结构来快速判断字符是否在窗口内出现过以及它的位置。哈希集合unordered_set或哈希映射unordered_map是理想选择。扩张右指针移动尝试将s[right]加入窗口。收缩与更新如果s[right]在窗口内已存在发生重复则必须收缩左边界将重复字符及其之前的字符全部移出窗口直到重复字符被移除。为了快速定位重复字符的位置我们使用哈希映射map[字符] - 该字符最新的索引位置。记录答案在每次右指针移动后即确保当前窗口无重复时更新最大长度。C 实现与逐行解读class Solution { public: int lengthOfLongestSubstring(string s) { int n s.size(); int left 0; int maxLen 0; // 哈希表记录字符最近一次出现的位置 unordered_mapchar, int charIndexMap; for (int right 0; right n; right) { char c s[right]; // 如果当前字符已经在哈希表中并且其索引位置大于等于 left即在当前窗口内 if (charIndexMap.find(c) ! charIndexMap.end() charIndexMap[c] left) { // 发生重复将左边界直接跳到重复字符的下一个位置 // 这样做比用 while 循环逐个移动 left 更高效 left charIndexMap[c] 1; } // 无论是否重复都要更新当前字符的最新位置 charIndexMap[c] right; // 计算当前无重复窗口的长度并更新最大值 maxLen max(maxLen, right - left 1); } return maxLen; } };实操心得哈希表的妙用这里使用unordered_map不仅是为了查询是否存在更是为了记录位置。当发现重复时我们可以直接将left跳到map[c] 1实现了窗口的“跳跃式”收缩这是比用unordered_set配合while循环更优的写法。判断条件charIndexMap[c] left这是关键。哈希表记录的是字符历史上最后一次出现的位置。这个位置可能已经在当前窗口的左边即left已经移过了它。只有重复字符的位置 left才说明它确实在当前窗口内需要处理。否则它只是一个“历史记录”不影响当前窗口的唯一性。更新时机的统一注意charIndexMap[c] right;这行代码放在if判断之后、计算长度之前。这保证了哈希表里永远存的是最新索引为下一次判断提供正确依据。4. 滑动窗口的通用模板与变形通过以上三题我们可以提炼出一个适用于大多数可变窗口问题的 C 模板框架int slidingWindowTemplate(vectorint nums, int target) { int left 0; // 窗口左边界 int result 0; // 存储答案可能是长度、计数等 int windowState 0; // 窗口状态如和、乘积、字符集等 for (int right 0; right nums.size(); right) { // 步骤1更新窗口状态加入 nums[right] windowState updateStateAdd(windowState, nums[right]); // 步骤2判断窗口状态是否“不合法” while (windowStateIsInvalid(windowState, target)) { // 步骤3更新窗口状态移除 nums[left] windowState updateStateRemove(windowState, nums[left]); left; // 收缩窗口 } // 步骤4此时窗口状态是“合法”的在此更新最终答案 result result updateResult(result, left, right); } return result; }这个模板中的四个函数updateStateAdd,windowStateIsInvalid,updateStateRemove,updateResult需要根据具体问题具体实现。例如在 209 题中windowState是和suminvalid条件是sum target注意我们是在while(sum target)里收缩所以invalid的反面就是循环条件。在 713 题中windowState是乘积productinvalid条件是product k。在 3 题中windowState是哈希表charIndexMapinvalid条件是当前字符已存在且位置 left。掌握这个模板你就能解决 LeetCode 上绝大多数滑动窗口问题如 76最小覆盖子串、567字符串的排列、904水果成篮等。5. 常见陷阱与调试技巧实录即便理解了算法实际编码时也难免踩坑。下面是我在练习和教学中总结的几个高频问题及解决方法。5.1 指针移动与状态更新的顺序这是最容易出错的地方。务必保证“更新状态”和“移动指针”的原子性。错误示范while (condition) { left; // 先移动指针 sum - nums[left]; // 再更新状态此时减的是新left的元素错了 }正确做法先更新状态基于旧的left再移动指针。while (condition) { sum - nums[left]; // 用当前的 left 索引更新状态 left; // 然后移动指针 }5.2 窗口合法性判断的边界条件以 209 题为例内层while的条件是sum target。如果写成if会怎样那样左指针只收缩一次可能无法将窗口收缩到最短导致得到的长度不是最小的。一定要用while循环确保窗口收缩到不再满足条件为止。对于 3 题判断重复时charIndexMap[c] left这个条件不可或缺。没有它对于字符串“abba”当right指向最后一个a时map[‘a’]是 0但此时left已经在 2 的位置因为之前处理过重复的b历史a不在当前窗口内所以不应该移动left。没有这个条件left会被错误地跳回 1导致计算长度出错。5.3 初始值与返回值处理最小值初始化像 209 题求最小长度初始值应设为INT_MAX这样第一次比较min()时才会被更新。如果初始为 0答案永远会是 0。最大值初始化反之求最大值如 3 题通常初始为 0。特殊输入判断如 713 题对k 1的判断能直接返回避免无意义计算。对于空数组输入循环不会执行返回初始值如 0 或INT_MAX是否合理需要根据题目要求考虑。通常题目保证非空但养成检查的习惯是好的。5.4 复杂状态维护与数据结构选择当窗口状态不仅仅是简单的和或乘积时选择合适的数据结构至关重要。字符统计/去重优先用unordered_set或unordered_mapO(1) 时间查询。需要维护窗口内有序元素如求中位数、最大值考虑使用平衡二叉搜索树如 C 的multiset或两个堆大顶堆小顶堆。但这会使得窗口滑动时增加 O(log n) 的更新开销。需要统计频率unordered_mapchar, int是最佳选择。一个调试小技巧在编写复杂窗口逻辑时可以在循环内打印left,right,windowState等关键变量模拟一个小例子肉眼观察窗口的滑动和状态变化是否符合预期。6. 从刷题到实战滑动窗口的工程应用滑动窗口绝不仅仅是算法题里的玩具。它在实际工程中有着广泛的应用理解其思想能帮你更好地设计系统。网络流量控制与限流这是滑动窗口的经典应用。例如你需要限制某个 API 在最近 1 秒内最多被调用 100 次。你可以维护一个时间窗口比如一个队列每次请求到来时将当前时间加入队列并移除队列中所有超过 1 秒的旧时间戳。然后检查队列长度是否超过 100。这就是一个基于时间的滑动窗口计数器。实时数据流分析在监控系统或实时计算中如 Apache Flink 中的滑动窗口你需要统计最近 N 分钟内的错误次数、交易总额等。滑动窗口算法可以高效地增量更新这些聚合指标而无需每次都重新扫描全部数据。日志去重或模式匹配在一段连续的日志流中查找符合某种模式如包含特定错误序列的片段。可以将日志序列视为字符串使用滑动窗口配合状态机进行匹配。TCP 协议TCP 的拥塞控制算法之一就涉及到滑动窗口的概念用于管理网络中的数据包发送和确认确保可靠传输和流量效率。当你再遇到需要处理“连续区间”、“最近一段时间”、“子串匹配”这类需求时不妨想一想能否用一个左指针和一个右指针维护一个窗口在数据流上滑过去这种思维方式的转变往往能带来性能的质的提升。滑动窗口的精髓在于“维护状态增量更新”。它教会我们的不仅是一个算法模板更是一种优化思维避免重复计算利用历史信息。把这三道题2097133的代码自己敲几遍理解每一行代码背后的意图再去找同类型的题目如 76 904 930 1004练习你就能真正掌握这把利器。在面试中当你清晰地说出“这可以用滑动窗口解决维护一个左指针 left 和一个右指针 right...”时面试官就知道你是个明白人了。