尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

LeetCode-Go 题解:480. Sliding Window Median 滑动窗口中位数——双堆与延迟删除实战解析

LeetCode-Go 题解:480. Sliding Window Median 滑动窗口中位数——双堆与延迟删除实战解析 LeetCode-Go 题解480. Sliding Window Median 滑动窗口中位数——双堆与延迟删除实战解析【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go本篇技术指南以 LeetCode-Go 仓库中 0480.Sliding-Window-Median/README.md 为核心结合仓库内的 Go 源码实现 与 单元测试系统讲解 LeetCode 480 题「滑动窗口中位数」的两种解法有序链表模拟法与双堆大顶堆 小顶堆加延迟删除法。读完本文你将掌握「动态维护有序数据流中位数」这一经典范式理解 Go 标准库container/heap的封装技巧以及解决堆中元素删除问题的通用方案并能直接复现本仓库的完整代码与测试。题目描述Median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the mean of the two middle value.Examples:[2,3,4]中位数是3[2,3]中位数是(2 3) / 2 2.5Given an arraynums, there is a sliding window of sizekwhich is moving from the very left of the array to the very right. You can only see theknumbers in the window. Each time the sliding window moves right by one position. Your job is to output the median array for each window in the original array.例如给定nums [1,3,-1,-3,5,3,6,7]且k 3Window position Median --------------- ----- [1 3 -1] -3 5 3 6 7 1 1 [3 -1 -3] 5 3 6 7 -1 1 3 [-1 -3 5] 3 6 7 -1 1 3 -1 [-3 5 3] 6 7 3 1 3 -1 -3 [5 3 6] 7 5 1 3 -1 -3 5 [3 6 7] 6因此返回的滑动窗口中位数数组为[1,-1,-1,3,5,6]。注意你可以假设k始终合法即对于非空数组k总是小于输入数组的长度。题目大意中位数是有序序列最中间的那个数。如果序列的大小是偶数则没有最中间的数此时中位数是最中间的两个数的平均数。给出一个数组nums有一个大小为k的窗口从最左端滑动到最右端。窗口中有k个数每次窗口移动 1 位。你的任务是找出每次窗口移动后得到的新窗口中元素的中位数并输出由它们组成的数组。解题思路总览原文档给出了三条解题路径复杂度差异明显解法核心思路时间复杂度空间复杂度暴力法每个窗口内整体排序取中间值O(n * K)O(K)解法一有序链表container/list维护窗口O(n * K)O(K)解法二双堆大顶堆 小顶堆 延迟删除O(n * log k)O(k)需要说明的是本题在 LeetCode 上属于 Hard 难度正是因为「滑动窗口」叠加了「中位数维护」两个难点。仓库的website站点的 ChapterTwo/Sliding_Window.md 表格中也把本题归入 Sliding Window 分类并标记了❤️值得重点掌握以及 O(n * log k) / O(k) 的复杂度标注。另外原文档明确指出「这一题是第 239 题的升级版」。第 239 题 0239.Sliding-Window-Maximum 只要求窗口内最大值可用双端队列 O(n) 解决而本题要求中位数窗口内中间元素的定位决定了其复杂度高于求极值这也是它成为 Hard 的关键所在。解法一有序链表模拟O(n * K)解法一最贴近题意维护一个始终有序的窗口链表每次滑动时先移除左端滑出的元素再按序插入右端新滑入的元素最后直接取链表中间位置的值作为中位数。源码位于 480. Sliding Window Median.go 的第 1064 行。初始化首窗口排序建链getWindowList将前k个元素拷贝、排序后依次压入双向链表func getWindowList(nums []int, k int) *list.List { s : make([]int, k) copy(s, nums) sort.Ints(s) l : list.New() for _, n : range s { l.PushBack(n) } return l }滑动删除 有序插入每次窗口右移一位需要做两件事removeFromWindow在链表中找到第一个值等于nums[p1-k]的元素并删除若找不到则原样返回保持链表不变insertInWindow从表头遍历把新元素插入到第一个「大于等于它」的元素之前从而维持链表有序。func insertInWindow(w *list.List, n int) *list.List { for e : w.Front(); e ! nil; e e.Next() { if e.Value.(int) n { w.InsertBefore(n, e) return w } } w.PushBack(n) return w }取中位数getMedian先让指针走到链表的第k/2个节点若k为奇数该节点即中位数若k为偶数中位数为该节点与其前驱节点的平均值。func getMedian(w *list.List, k int) float64 { e : w.Front() for i : 0; i k/2; e, i e.Next(), i1 { } if k%2 1 { return float64(e.Value.(int)) } p : e.Prev() return (float64(e.Value.(int)) float64(p.Value.(int))) / 2 }复杂度分析每次插入 / 删除都需要 O(k) 遍历链表定位窗口共滑动 n-k1 次因此整体时间复杂度 O(n * K)空间复杂度 O(K)。此解法胜在直观、完全贴合题意但性能不足以应对大数据量。解法二双堆 延迟删除O(n * log k)这是本题的标准最优解也是原文档重点阐述的思路用两个优先队列堆记录窗口内的值大顶堆maxH里的元素都比小顶堆minH里的元素小即小顶堆存放排序后中间靠后、值偏大的那一半大顶堆存放中间靠前、值偏小的那一半若k为偶数两个堆各放k/2个元素中位数 两个堆顶元素的平均值若k为奇数小顶堆比大顶堆多一个元素中位数 小顶堆堆顶元素删除窗口滑出的元素时并不真正从堆中物理移除而是把该元素**标记到对应堆的删除堆**中取top时不断弹出已被标记的元素保证堆顶始终是有效元素。底层堆的封装IntHeap / MinHeap / MaxHeap源码先用container/heap接口封装了一个基础数组堆IntHeap再通过内嵌与覆写Less的方式派生出MinHeap和MaxHeaptype IntHeap struct { data []int } func (h IntHeap) Len() int { return len(h.data) } func (h IntHeap) Swap(i, j int) { h.data[i], h.data[j] h.data[j], h.data[i] } func (h *IntHeap) Push(x interface{}) { h.data append(h.data, x.(int)) } func (h *IntHeap) Pop() interface{} { x : h.data[h.Len()-1] h.data h.data[0 : h.Len()-1] return x } func (h IntHeap) Top() int { return h.data[0] } type MinHeap struct{ IntHeap } func (h MinHeap) Less(i, j int) bool { return h.data[i] h.data[j] } type MaxHeap struct{ IntHeap } func (h MaxHeap) Less(i, j int) bool { return h.data[i] h.data[j] }关键点Less决定堆序。MinHeap用小顶堆序MaxHeap用大顶堆序Top()直接取数组首元素data[0]即堆顶。可删除堆MinHeapR / MaxHeapR延迟删除的核心结构是「真实堆 删除标记堆」的组合type MinHeapR struct { hp, hpDel MinHeap } func (h MinHeapR) Len() int { return h.hp.Len() - h.hpDel.Len() } func (h *MinHeapR) Top() int { for h.hpDel.Len() 0 h.hp.Top() h.hpDel.Top() { heap.Pop(h.hp) heap.Pop(h.hpDel) } return h.hp.Top() } func (h *MinHeapR) Pop() int { x : h.Top() heap.Pop(h.hp) return x } func (h *MinHeapR) Push(x int) { heap.Push(h.hp, x) } func (h *MinHeapR) Remove(x int) { heap.Push(h.hpDel, x) }要点解读Len()是有效长度真实堆大小减去已标记删除的元素个数Remove(x)只是入删除堆O(log k) 完成一次伪删除并不真正调整真实堆结构Top()负责清扫只要删除堆堆顶与真实堆堆顶相等就同时弹出这两个堆顶直到真实堆顶是有效元素。由于大小堆的堆顶分别是最小值 / 最大值被标记删除的元素只有在轮到自己当堆顶时才会被真正清除这正是延迟删除的精髓MaxHeapR的实现完全对称不再赘述。主流程逐步解析medianSlidingWindow1一次遍历完成插入 → 移除 → 再平衡 → 求中位数四个动作func medianSlidingWindow1(nums []int, k int) []float64 { ans : []float64{} minH : MinHeapR{} maxH : MaxHeapR{} for i : range nums { if minH.Len() 0 || nums[i] minH.Top() { minH.Push(nums[i]) } else { maxH.Push(nums[i]) } if i k { if nums[i-k] minH.Top() { minH.Remove(nums[i-k]) } else { maxH.Remove(nums[i-k]) } } if minH.Len() maxH.Len()1 { maxH.Push(minH.Pop()) } else if minH.Len() maxH.Len() { minH.Push(maxH.Pop()) } if minH.Len()maxH.Len() k { if k%2 0 { ans append(ans, float64(minH.Top()maxH.Top())/2.0) } else { ans append(ans, float64(minH.Top())) } } } return ans }逐环节说明插入若小顶堆为空或新元素不小于小顶堆堆顶即不小于当前较小的一半的最大值则进入小顶堆否则进入大顶堆。这一步保证大顶堆里所有元素 ≤ 小顶堆里所有元素移除当i k时窗口已满此时nums[i-k]是滑出窗口的元素。判断它原本属于哪一侧与minH.Top()比较后调用对应堆的Remove做延迟删除再平衡保持minH.Len()要么等于maxH.Len()k 为偶数要么多 1k 为奇数使小顶堆堆顶恰好是窗口内中间靠右的元素求中位数仅当两堆有效元素之和等于k即当前窗口完整时按 k 的奇偶输出结果。偶数取(minH.Top() maxH.Top()) / 2.0注意转成float64再做浮点除法奇数直接取minH.Top()。一个极易踩坑的细节是minH.Len() maxH.Len() k中的Len()是扣除删除标记后的有效长度因此该判断天然过滤了窗口尚未填满的早期阶段。复杂度分析每个元素至多入堆、出堆、入删除堆各一次每次堆操作 O(log k)总时间复杂度 O(n * log k)空间上两堆各存 O(k)整体 O(k)。这正是本仓库在网站表格中标注的复杂度。测试验证与覆盖细节仓库为本题编写了覆盖完整的单元测试见 480. Sliding Window Median_test.go。测试用例精心设计逐一印证了两种解法的一致性用例输入期望输出覆盖点奇数窗口[1,3,-1,-3,5,3,6,7], k3[1,-1,-1,3,5,6]题目标准示例覆盖 k 为奇数分支偶数窗口同上数组, k4[0,1,1,4,5]覆盖k%20时取两堆顶均值的分支重复元素[5,2,2,7,3,7,9,0,2,3], k3[2,2,3,7,7,7,2,2]大量重复值覆盖MaxHeapR.Top中删除堆顶去重循环反复触发的情况边界分支窗口[1,2,3]中移除不存在的 99链表长度保持 3覆盖removeFromWindow中元素不存在时的返回分支测试框架的做法是对每组用例同时调用链表解法medianSlidingWindow与双堆解法medianSlidingWindow1先校验输出长度一致再逐位比对结果二者互为印证杜绝了单解法实现偏差。你可以通过仓库根目录的 gotest.sh 提供的命令验证整个 leetcode 包的覆盖率与正确性go test -covermodeatomic -coverprofilecoverage.txt ./leetcode/...或只针对本题快速验证go test -v -run Test_Problem480 ./leetcode/0480.Sliding-Window-Median/注意本仓库源码位于leetcode包内测试函数名为Test_Problem480运行测试时需要以leetcode包为单位执行。小结暴力法窗口内每次排序O(n * K)仅适用于小数据量解法一有序链表以 O(n * K) 的代价换来了完全贴合题意的直观实现是理解题意和验证结果的最佳辅助解法二双堆 延迟删除借助container/heap将插入、删除、取中位数全部压到 O(log k)整体 O(n * log k) / O(k)是本题的标准最优解通用范式MinHeapR / MaxHeapR这套「真实堆 删除标记堆」的延迟删除设计可复用于任何滑动窗口 有序统计类题目如求窗口第 K 大、窗口最频繁元素等也适用于数据流场景中需要删除过期元素的堆结构问题。建议读者在阅读本文后亲手在 源码文件 中走一遍nums [1,3,-1,-3,5,3,6,7], k 3的完整推演并运行测试用例对比两种解法的输出从而真正掌握双堆维护中位数的核心技巧。【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表