首页/新闻资讯/正文详情

C++滑动窗口最大值:单调队列从原理到实战

发布时间:2026/9/26 14:13:31 来源:云帆数科 栏目:资讯中心
C++滑动窗口最大值:单调队列从原理到实战
这是我这轮C刷题打卡的第19篇。今天要拆的这道题是滑动窗口最大值LeetCode 239在面试里属于较高频的题目而且它背后那个“单调队列”的思路几乎可以平移套用到一整个滑动窗口题型家族。题目描述特别简短给你一个整数数组 nums 和一个大小为 k 的滑动窗口窗口每次向右移动一格要求返回每个窗口内数值的最大值。不少新手第一反应是“每个窗口扫一遍找最大值不就行了”但数据规模一大暴力做法立马超时。这也是为什么这道题值得单独打卡记录。这篇文章会从头梳理完整思路为什么暴力解法不是终点单调队列是怎么一步步设计出来的用C实现时有哪些必须注意的细节以及我在刷题过程中真实踩过的边界条件的坑。无论你是刚开始刷C算法题的新手还是想快速复习滑动窗口套路的人都可以对照着把这道题彻底吃透。1. 读懂题意滑动窗口最大值到底在问什么1.1 题目描述与示例手推先把题目用大白话讲清楚。给定一个数组 nums比如 [1,3,-1,-3,5,3,6,7]再给一个固定大小 k3。窗口最开始覆盖数组前3个元素也就是索引0到2。每次把窗口向右挪一格索引0出去索引3进来再挪一格索引1出去索引4进来。每挪一次都把窗口里这3个数的最大值记下来。最后输出的数组长度是 n-k1。手动推一遍这组数据窗口1[1,3,-1] → 最大值3窗口2[3,-1,-3] → 最大值3窗口3[-1,-3,5] → 最大值5窗口4[-3,5,3] → 最大值5窗口5[5,3,6] → 最大值6窗口6[3,6,7] → 最大值7最终输出就是 [3,3,5,5,6,7]。这里可以看到几个关键事实窗口只前进不回头每次只有最左边一个元素离开、最右边一个元素进来如果某次离开的是当前最大值下一个窗口的最大值就得重新找如果新进来的元素足够大它会立刻成为新窗口的最大值。这些看似废话的观察恰恰是后面单调队列设计的出发点。1.2 暴力解法为什么一定不是终点新手最容易想到的解法外层循环枚举每个窗口起点内层循环从窗口起点扫到起点k找最大值。时间复杂度 O(nk)。如果 n10^5、k10^4最坏情况下要做 10^9 次比较在普通评测机上基本就是 TLE 的下场。有人可能想优化用变量记录当前窗口最大值窗口移动时如果新元素比它大就更新但问题出在最大值滑出窗口的那一刻。你不知道窗口里次大值是谁只能重新扫描一遍整个窗口找最大值。最坏情况完全可以构造出来比如数组是一个递减序列最大值频繁从左侧滑出窗口每移动一次都触发一次重新扫描复杂度还是 O(nk)。所以这道题真正要求的是一趟遍历解决每个元素进窗口、出窗口各一次在常数时间内维护出当前窗口的最大值。这就是单调队列要干的事。2. 单调队列让“过期最大值”自动滚出窗口2.1 用排队候选人的思维理解单调队列想象你是一个窗口管理员手里维护着一个“候选最大值队列”里面只放那些有可能成为当前窗口最大值的元素。队首永远是最有资格当最大值的候选人。当新元素进窗口时规则只有两条新元素从队尾入队前把队尾所有比它小或等于它的元素全部淘汰。因为新元素更大或相等而且比它们更晚过期队尾那些元素从此刻起永远不可能再成为窗口最大值留着只会占位置。窗口左端滑出一个元素时如果这个元素正好是当前队列的队首说明窗口最大值被滑出去了直接弹出队首如果不是队首说明它早就被后面某个更大的元素淘汰了本来就不在队列里根本不需要额外处理。用刚才的例子手推一遍。nums [1,3,-1,-3,5,3,6,7]k 3。处理索引0值1队列空1入队。队列[1]处理索引1值3队尾1 3淘汰13入队。队列[3]处理索引2值-1队尾3 -1-1入队。队列[3,-1]。窗口已满答案是3处理索引3值-3队尾-1 -3-3入队。同时索引0早已被淘汰无需清理。窗口最大值仍为3。队列[3,-1,-3]处理索引4值5从队尾开始-3淘汰、-1淘汰、3也淘汰5入队。队列[5]。答案是5处理索引5值35 33入队。队列[5,3]。答案是5处理索引6值63淘汰、5淘汰6入队。队列[6]。答案是6处理索引7值76淘汰7入队。队列[7]。答案是7这个队列从头到尾永远是非递增的队首最大队尾最小索引从小到大排列。它维护的其实是一个“窗口内所有潜在最大值”的候选名单跟“在窗口内且未被更大元素压制”的元素。2.2 为什么必须用deque两端O(1)操作是硬要求从上面的过程可以看出候选队列需要支持四种操作队尾弹出淘汰较小元素、队尾插入新元素入队、队首弹出最大值滑出窗口、读取队首获取答案。前三个操作都要求常数时间。C 标准库里恰好有一个容器完美匹配这些要求std::deque双向队列。它支持两端的常数时间插入和删除底层是分段连续缓冲区虽然实现比 vector 复杂但对我们使用者来说只管接口。如果用 vectorpop_front 是 O(n)不行用 list两端操作虽然 O(1)但内存分散、常数开销更大而且这道题并不需要 list 的节点稳定性。实际刷题时 deque 是标准答案。我第一次做这题时还想过优先队列。堆确实能维护最大值但堆不方便处理“某个元素滑出窗口”的问题堆顶过期了要等它被弹出才能发现而且堆里没法快速删除任意元素。优先队列需要配合延迟删除复杂度会变成 O(n log n)明显比单调队列复杂常数也更大。单调队列把过期判断从“主动找”变成“被动等”这是它最妙的地方。3. 完整C实现单调队列的落地代码3.1 参考代码带注释的完整实现直接给出我最终 AC 的版本附带完整注释#include vector #include deque using namespace std; class Solution { public: vectorint maxSlidingWindow(vectorint nums, int k) { int n nums.size(); dequeint q; // 队列里存的是索引不是值 vectorint ans; ans.reserve(n - k 1); for (int i 0; i n; i) { // 第一步新元素入队前淘汰所有比它小的队尾元素 while (!q.empty() nums[q.back()] nums[i]) { q.pop_back(); } q.push_back(i); // 第二步把已经滑出窗口的队首索引清掉 while (!q.empty() q.front() i - k) { q.pop_front(); } // 第三步窗口完全形成后记录窗口最大值 if (i k - 1) { ans.push_back(nums[q.front()]); } } return ans; } };ans.reserve(n - k 1) 是顺手加上去的提前分配好容量可以避免 vector 反复扩容带来的拷贝开销。刷 LeetCode 时不明显但对性能敏感的场景这是好习惯。3.2 关键逻辑逐行拆解为什么存索引不存值最核心的一点队列里存的是索引不是值。这可能是整道题最容易忽略、也最容易出 bug 的地方。原因有两个。第一窗口滑出时需要精确判断“哪个元素该走”。如果只存值遇到重复元素会分不清谁是谁。比如窗口 [2,2,3]最大值3滑出后窗口变成 [2,2]队里还剩两个2如果存的是值你根本不知道要删哪个2。存索引可以保证每个元素唯一q.front() 是否等于滑出位置一目了然。第二过期判断 q.front() i - k 本身就需要索引参与只看值是无法判断“是否过期”的。再看第一步为什么用 而不是 当新元素等于队尾元素时队尾旧元素同样应该淘汰。旧元素比新元素更早过期而新元素作为最大值相等也算能存活更长时间旧元素此后永远不可能成为窗口最大值留着只会增加队列长度。用 能保证队列里没有冗余的相等元素。第二步的过期条件值得细说。窗口滑动到当前位置 i 时窗口覆盖的索引范围是 [i-k1, i]。所以索引 i-k 的元素都已经在窗口左边界之外必须清掉。很多人纠结为什么不是 q.front() i-k1其实两者完全等价因为整数区间的关系可以去等号边界。我习惯写 i-k因为 i-k 正好是窗口外最后一个位置语义更直白。第三步的时机i k-1 表示窗口已经覆盖了前 k 个元素从这一刻起往后每次移动都要记录答案。这里直接从 i 的角度判断比写 i-k1 0 更直观也避免踩无符号数比较的坑下面会专门说。3.3 复杂度分析与性能实测时间上每个元素最多进队一次、出队一次所以队列操作总数 O(n)遍历数组本身也是 O(n)整体时间复杂度 O(n)。空间上队列里最多同时存在 k 个元素因为队首到队尾的索引跨度不会超过窗口大小超出窗口的早被第二步清掉了空间复杂度 O(k)。我用随机数据简单实测过n10^5、k10^4暴力法在我的机器上要跑一秒多甚至更久单调队列版本基本在1ms级别差距接近千倍。更极端一点n10^6、k10^5暴力法几乎没法用单调队列仍然毫秒级。这也是为什么 O(nk) 到 O(n) 这道坎在刷题里是决定性的。4. 实战中的坑边界、索引与编译器配置4.1 三个最容易写错的边界条件边界条件这种东西没踩过坑之前总觉得“不就几个 if 吗”踩过之后才知道全在细节里。k1 的情况。窗口就一个元素答案就是原数组本身。用上面的代码跑一遍每个 i 进来第一步会把前一个元素淘汰push i第二步 q.front() i-1 会把前一个索引清走第三步 i 0 直接输出 nums[i]。输出结果正好是原数组逻辑完全自洽。kn 的情况。整个数组只有一个窗口正确答案是数组的全局最大值。代码里第二步的过期条件是 i-k只有当 in-1 时 i-k 才等于0之前的索引不会被清走第三步只在最后一个元素处输出一次此时队首正好是全局最大值。n0 或 kn 的情况。按题目数据范围可能不会出现但严谨起见应该在函数开头加防御逻辑if (nums.empty() || k n) return {};k 为0也可以直接返回空防御性写法能避免线上用例莫名其妙踹你一脚。刷题代码可以省略但工程习惯会提醒你写上。4.2 排查实录size_t下溢与重复值误删这里分享一个我亲眼见过很多次的坑很多人写循环判断时喜欢用 nums.size() 而不是先保存 n。比如for (int i 0; i nums.size() - k 1; i)当 k 大于 nums.size() 时nums.size() - k 1 是 size_t 类型无符号数减法会先发生下溢变成一个巨大的正数结果就是循环直接变成无意义的长跑要么超时要么越界访问。这是 C 里比边界条件更隐蔽的坑因为本地编译不报错甚至小数据量都不触发一提交就莫名其妙 WA 或 RE。我的建议很朴素函数开头写 int n nums.size()后面所有下标和循环全部用 int。刷题场景的数据规模一般不会超过 int 范围这样做从根源上避开无符号数参与算术的隐患。重复值误删这个问题我在 3.2 里已经解释过原理。第一次提交时我存的就是值遇到 [2,2,3] 这种用例直接挂。当时调试了很久才反应过来不是算法逻辑错而是队列里根本没法区分两个相同的2。改成存索引后这道题再也没在重复值上出过问题。如果你已经写过一版存值的版本可以自己跑一下这个用例体会会很深。4.3 顺手解决VS Code的IntelliSense报红刷题打卡时很多人用 VS Code最烦的就是代码能编译通过编辑器里却红波浪线不断。最典型的就是“vscode cpp头文件错误报红”这通常不是代码问题而是 IntelliSense 没找到 C 标准库头文件的路径。最简单的处理有两种。一是如果你的编译器是 g在 .vscode/c_cpp_properties.json 里配置 compilerPath 指向 g 的绝对路径并把 includePath 加上编译器自带的 include 目录。配置参考长这样{ configurations: [ { name: Win32, includePath: [ ${workspaceFolder}/**, C:/Program Files/mingw-w64/include/c/** ], compilerPath: C:/Program Files/mingw-w64/bin/g.exe, cppStandard: c17, intelliSenseMode: windows-gcc-x64 } ], version: 4 }二是如果你跟我一样刷题时不想折腾编辑器配置直接在设置里把波浪线关了C_Cpp.errorSquiggles 设为 disabled。反正代码对不对最终以编译器运行结果为准IntelliSense 的波浪线只是参考没必要让它影响心情。运行编译时我一般用这个命令简单直接g -stdc17 -O2 -o main main.cpp ./main-O2 优化在刷题验证算法性能时比较接近评测机的真实表现。5. 从这题看滑动窗口题型的通用套路与打卡复盘5.1 滑动窗口题型的通用三板斧这道题做完最大的收获是能抽象出滑动窗口类题目的通用思路。我总结成三步几乎可以套用到绝大多数滑动窗口题。第一想清楚窗口的扩张和收缩规则。每次移动过程中哪些元素进窗口、哪些元素出窗口是决定算法正确性的基础。第二决定用什么数据结构维护当前窗口的“状态”。这个状态可以是最大值单调队列、最小值还是单调队列、字符种类数哈希表双指针、区间和前缀和等等。第三确定结果收集的时机通常是在窗口第一次完整形成之后每次移动都记录一次。举两个可以直接套这个思路的题目。LeetCode 3 最长无重复子串双指针维护窗口哈希表记录字符出现次数右指针扩张、左指针收缩每次移动都更新答案。LeetCode 1438 绝对差不超过限制的最长连续子数组需要同时知道窗口最大值和最小值用两个单调队列分别维护右指针扩张窗口左指针在最大最小差超限时收缩。这两个题本质上都是“窗口状态维护”的变体理解了239再写这两道会顺手很多。5.2 打卡复盘19天记录让我学到了什么最后聊聊打卡这件事本身。我的每篇记录里固定写三块内容题目复述和我自己的第一反应、最终 AC 代码、踩坑笔记。今天回看第19篇最有价值的其实不是 AC 代码本身而是那些错误提交记录。比如这道题我第一次用值存储在重复元素用例上挂了三个测试点后来凡是单调队列题型我写代码前都会先问自己一句队列里应该存索引还是存值这个习惯就是靠记录养成的不把每次错误记下来下次遇到类似的坑还是会在同一个地方摔倒。如果你也在做类似的刷题打卡我建议别只贴代码至少写一句“为什么这么写”。不用写得多漂亮哪怕只是“因为存值会误删”这六个字三个月后回看都会很有价值。记录的意义从来不是给别人看是给未来的自己留一条捷径。我的下一道题打算继续做滑动窗口的变种到时候再来更新新的打卡记录。

相关推荐

VC远程控制源码解析:WINLOGON与GetInfo双工程实战
VC远程控制源码解析:WINLOGON与GetInfo双工程实战

简介:这份资源是面向VC初学者与网络编程进阶者的远程控制软件完整源码包,基于Visual C与Windows API实现,帮助读者理解屏幕共享、文件传输、键鼠模拟等远程控制核心功能的底层原理。压缩包共30个文件,约37KB,以h头文件… · 2026/9/26 14:13:31

AI大模型API聚合平台企业选型:权限管控、审计日志与发票合规
AI大模型API聚合平台企业选型:权限管控、审计日志与发票合规

API聚合与调度平台已经演变为关键数字基础设施,不再只是流量的统一入口:一次服务中断可能导致生产流水线停摆,模糊计费会埋下财务审计隐患。对企业用户而言,选型的权重排序与个人开发者完全不同,权限管控、审计日志与发票合规是三道硬门槛。本文从企业视角展开,第一个推荐的平台… · 2026/9/26 14:13:24

一站式大模型聚合网关:分层架构与企业级可观测性落地方案
一站式大模型聚合网关:分层架构与企业级可观测性落地方案

连接开发者与全球大模型的中间层,在2026年有了清晰的工程形态:一站式聚合统一接口网关。它要同时解决多模型调用繁琐、官方账号难申请、网络不稳定、成本偏高四大行业痛点。本文以词元之河(TokenRiver.ai)的实践为样本,拆解这类网关的分层架构与企业级可观测性设计。一个账号、… · 2026/9/26 14:13:24

前后台分离的仓库管理系统课设实战:Android+Spring Boot从零到答辩
前后台分离的仓库管理系统课设实战:Android+Spring Boot从零到答辩

简介:一套基于Android Studio实现前后台分离的仓库管理系统完整源码项目,面向移动应用开发初学者、课程设计学生及需要参考完整Android项目的开发者。系统按角色划分超级管理员、出入库人员和商品管理员,覆盖注册登录、用户管理、商品增删查、… · 2026/9/26 14:52:11

Atlas 300V 24G部署YOLO实战:从ONNX到OM的昇腾推理全攻略
Atlas 300V 24G部署YOLO实战:从ONNX到OM的昇腾推理全攻略

1. Atlas 到底是什么:先给 300V 24G 验明正身 先说一个很多人刚接触时都会犯的迷糊: Atlas 不是一个单一的硬件型号,而是华为昇腾(Ascend)AI 计算平台的整体品牌名 。它底下有板卡、模组、服务器、加速模块好几条产品… · 2026/9/26 14:52:11

昇腾Atlas 300V 24G推理卡实战:YOLO模型部署与调优全攻略
昇腾Atlas 300V 24G推理卡实战:YOLO模型部署与调优全攻略

1. 先回答热搜问题:Atlas 300V 24G到底是什么卡 先说结论: Atlas 300V 24G是一张不折不扣的AI推理加速卡,不是显卡,也不是训练卡。 最近这个热搜词我看到了,很多人把它和游戏显卡、图形工作站显卡混为一谈&#xff… · 2026/9/26 14:52:11

open-code-review开源实践:搭建AI智能代码审查流程与CI门禁
open-code-review开源实践:搭建AI智能代码审查流程与CI门禁

代码审查这事儿,干了十年的人都有个共识:它是保证代码质量最有效的手段,但同时也是团队里最容易被延期、被跳过、被敷衍的环节。不是大家不想做,是实在抽不出整块时间在PR列表里翻来覆去地比对上下文。尤其项目一忙起来&#xff0… · 2026/9/26 14:52:11

Atlas 300V 24G部署YOLO实战:从推理卡环境搭建到性能优化
Atlas 300V 24G部署YOLO实战:从推理卡环境搭建到性能优化

最近工作室来了张Atlas 300V 24G,正好手里有几个YOLO检测项目要落地。折腾了几天,从装卡、配置环境到把模型跑起来,中间踩了不少坑,也摸到了一些门道。这篇就把我拿这张运算加速卡部署YOLO的完整过程写出来,包括硬件安… · 2026/9/26 14:52:11

PowerShell执行策略拦下npm.ps1?一文看懂报错根因与OpenClaw安装破解法
PowerShell执行策略拦下npm.ps1?一文看懂报错根因与OpenClaw安装破解法

如果你在Windows上安装OpenClaw,或者任何依赖npm的Node项目,很大概率会在终端里撞见这么一堵墙:npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。我第一次遇到这个报错时也愣了一下,… · 2026/9/26 14:52:05

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码