多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

LeetCode 239 滑动窗口最大值(Sliding Window Maximum):从暴力扫描到单调队列的 O(n) 解法全解

LeetCode 239 滑动窗口最大值(Sliding Window Maximum):从暴力扫描到单调队列的 O(n) 解法全解 LeetCode 239 滑动窗口最大值Sliding Window Maximum从暴力扫描到单调队列的 O(n) 解法全解【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode本文以 hints/sliding-window-maximum.md 的提示线索为主线结合仓库中 articles/sliding-window-maximum.md 的完整题解与 Python、Java、C、Go、TypeScript 等多语言实现系统讲解 LeetCode 239「滑动窗口最大值」的四种主流解法。读完本文你将掌握从 O(n·k) 暴力法到 O(n) 单调队列的完整优化链路理解堆的懒删除技巧并能对照仓库源码写出可运行的答案。题目与前置知识给定整数数组nums和滑动窗口大小k窗口每次向右移动一位要求返回每个窗口中的最大值。以仓库 cpp/0239-sliding-window-maximum.cpp 注释中的经典示例为例nums [1,3,-1,-3,5,3,6,7], k 3 - [3,3,5,5,6,7]该题属于 NeetCode Roadmap 的Sliding Window分类见 README.md 中 0239 一行的归类需要先掌握以下基础滑动窗口Sliding Window维护一个固定大小的窗口在数组上右移双端队列Deque支持 O(1) 时间在两端插入/删除单调队列Monotonic Queue队列内元素按值单调有序用于范围最值查询堆/优先队列Heap/Priority Queue配合懒删除快速取最值。复杂度目标为什么 O(n·k) 不够好提示文档开篇给出的目标很明确hints/sliding-window-maximum.md 的 Recommended Time Space Complexity你的解法应达到或优于O(n log n) 时间、O(n) 空间其中 n 为数组长度。这是一个关键约束信号朴素的双重循环做法是 O(n·k)当 k 接近 n 时会退化到 O(n²) 级别无法通过大数据量用例。因此必须寻找能够在 O(1) 时间获取窗口当前最大值的数据结构这正是后面堆与单调队列两条路线的出发点。Hint 1暴力法的瓶颈在哪暴力解法是对每个窗口起点i范围0到len(nums) - k再扫描窗口内全部k个元素找最大值class Solution: def maxSlidingWindow(self, nums: List[int], k: int) - List[int]: output [] for i in range(len(nums) - k 1): maxi nums[i] for j in range(i, i k): maxi max(maxi, nums[j]) output.append(maxi) return output复杂度时间 O(n·k)额外空间 O(1)输出数组另计 O(n - k 1)。问题在于相邻窗口有 k-1 个元素重叠暴力法每次滑动都把这些重叠元素重新扫描一遍造成大量重复计算。Hint 1 提示我们思考能否用某种数据结构让窗口的最大值在O(1) 时间内直接拿到Hint 2用最大堆记录 (值, 下标)处理最大值/最小值最自然的数据结构是堆取堆顶最值只需 O(1)。此处应使用最大堆。但这里有一个陷阱——堆中的元素会过期窗口左移后旧的最大值可能已经不在当前窗口内。Hint 2 的提示是换一种入堆方式——不要把裸值入堆而是把(value, index)二元组一起入堆用下标来判断元素是否仍属于当前窗口。以 Python 实现为例见 articles/sliding-window-maximum.md Heap 一节class Solution: def maxSlidingWindow(self, nums: List[int], k: int) - List[int]: heap [] output [] for i in range(len(nums)): heapq.heappush(heap, (-nums[i], i)) # Python 无原生最大堆取负模拟 if i k - 1: while heap[0][1] i - k: # 下标过期的堆顶直接弹出 heapq.heappop(heap) output.append(-heap[0][0]) return output仓库中其他语言的对应实现都遵循同一思路Javajava/0239-sliding-window-maximum.javaDeque 版Ccpp/0239-sliding-window-maximum.cpppriority_queuepairint,intGogo/0239-sliding-window-maximum.go自定义MaxHeap [][2]intRustrust/0239-sliding-window-maximum.rsBinaryHeap(i32, usize)Hint 3 Hint 4堆的懒删除Lazy Deletion窗口每滑动一步都会有元素从左侧离开窗口但它们仍留在堆里。Hint 3 提出的问题是如何高效处理这种幽灵元素Hint 4 给出了答案只要堆顶最大值仍在当前窗口内就可以忽略那些已过期但还留在堆里的元素只有当最大值本身过期时才不断弹出堆顶直到堆顶属于当前窗口。因为被过期元素挡在下面的有效元素迟早会在它成为堆顶时被清理因此不需要在滑动时逐一删除过期元素。这种先留着、等到碍事再删的手法就是经典的懒删除lazy deletion。堆解法整体复杂度时间 O(n log n)每个元素入堆/出堆各一次空间 O(n)。这正好满足提示文档给出的目标上界。最优解单调双端队列DequeO(n) 时间如果说堆解法已经达标那么单调队列则把时间进一步压到O(n)——这也是提示文档暗示的进阶方向。核心思想用双端队列只存下标且队列中下标对应的值严格递减。由此保证队首永远是当前窗口最大值的下标新元素入队时从队尾弹出所有值更小的下标——它们永远不可能成为未来窗口的最大值它们被新元素挡住且更早过期留着无用若队首下标已滑出窗口l q[0]从队首弹出。以仓库 python/0239-sliding-window-maximum.py 的实现为范本class Solution: def maxSlidingWindow(self, nums: List[int], k: int) - List[int]: output [] q collections.deque() # 存下标 l r 0 while r len(nums): # 弹出队尾所有值更小的下标它们不可能成为最大值 while q and nums[q[-1]] nums[r]: q.pop() q.append(r) # 队首若已滑出窗口则移除 if l q[0]: q.popleft() # 窗口达到 k 时记录答案 if (r 1) k: output.append(nums[q[0]]) l 1 r 1 return output各语言的落地版本均可直接对照阅读Javajava/0239-sliding-window-maximum.javaDequeIntegerfor循环版判断过期条件为q.peekFirst() i - k 1Ccpp/0239-sliding-window-maximum.cppGogo/0239-sliding-window-maximum.goTypeScripttypescript/0239-sliding-window-maximum.ts复杂度每个元素至多入队一次、出队一次均摊O(n) 时间O(n) 空间。这是本题的最优解法也是面试中应优先给出的答案。补充视角线段树与分块预处理O(n log n) / O(n)提示文档的主线是堆/队列但完整题解 articles/sliding-window-maximum.md 还提供了两种值得了解的替代方案线段树Segment Tree把区间最大值预处理进一棵线段树每个内部节点存左右子区间最大值的较大者随后对每个窗口[i, i k - 1]做一次 O(log n) 的区间最值查询。整体时间 O(n log n)空间 O(n)。构建时叶子放在数组后半段、内部节点自底向上取 max 的写法在题解中有完整 Python/Java/C/Go/Rust 实现。分块 DPleftMax rightMax把数组按块大小 k 切分分别预处理每个块内从左到右leftMax和从右到左rightMax的前缀/后缀最大值。任一窗口[i, ik-1]的最大值 max(leftMax[i k - 1], rightMax[i])即每块窗口的左右两半各取自一个预处理数组。时间 O(n)、空间 O(n)无需任何高级数据结构。常见坑Common Pitfalls根据题解末尾的总结以及仓库各语言实现的对比写代码时最容易踩的坑如下队里存值不存下标存值就无法判断元素是否已经滑出窗口。务必存下标取值时用nums[index]。窗口边界判断错误第一个合法窗口在right k - 1等价于right 1 k时才出现k与k-1差一会导致漏掉第一个窗口或过早记录结果。未维护单调递减弹出条件写错如用代替或忘记弹出队首就不再是真实最大值。正确写法是while 队尾对应值 当前值: 弹出队尾。输出数组大小算错窗口数量是n - k 1不是n或n - k当k n时结果恰好只有一个元素。线段树区间边界混淆查询区间[i, i k - 1]两端都含搞混闭/开区间会多查或少查元素。小结与仓库索引回到提示文档的复杂度目标O(n log n) 时间 / O(n) 空间。本文给出的两条达标路线是——最大堆 懒删除O(n log n)以及单调双端队列O(n)更优。暴力法O(n·k)仅用于建立直觉。如需在本地复现与练习可直接查阅以下仓库文件提示线索hints/sliding-window-maximum.md完整图文题解5 种解法 多语言代码articles/sliding-window-maximum.md单调队列实现Python python/0239-sliding-window-maximum.py、Java java/0239-sliding-window-maximum.java、C cpp/0239-sliding-window-maximum.cpp、Go go/0239-sliding-window-maximum.go、TypeScript typescript/0239-sliding-window-maximum.ts【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表