滑动窗口中位数
480. 滑动窗口中位数
中位数是有序序列最中间的那个数。如果序列的长度是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。
例如:
[2,3,4],中位数是 3
[2,3],中位数是 (2 + 3) / 2 = 2.5
给你一个数组 nums,有一个长度为 k 的窗口从最左端滑动到最右端。窗口中有 k 个数,每次窗口向右移动 1 位。你的任务是找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。
给出 nums = [1,3,-1,-3,5,3,6,7],以及 k = 3。
窗口位置 中位数 --------------- ----- [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 始终小于输入的非空数组的元素个数。
- 与真实值误差在 10 ^ -5 以内的答案将被视作正确答案。
来源:力扣(LeetCode) https://leetcode-cn.com/problems/sliding-window-median
题解
【思路】
看到计算 中位数 ,首先想到的是 堆 。将较小的半部分放在大顶堆中,较大的部分放在小顶堆中,并且满足「小顶堆大小」与「大顶堆大小」的平衡关系,即可很方便的取得中位数。顺着这个思路我们需要做的是:
- 提供大顶堆和小顶堆;
- 添加数据,并且要平衡大小顶堆的大小;
- 删除数据,并且要平衡大小顶堆的大小;
- 获取中位数,本题中也就是根据滑动窗口大小取堆顶元素(窗口大小为奇数,则中位数为小顶堆堆顶;为偶数,则取两堆顶的平均值)。
现在问题又来了,从堆中删除指定数据是件麻烦的事,堆结构只能删除堆顶元素。
解决的一种方法就是 延迟删除 ,我们先记下来该元素待删除,当该元素在堆顶的时候就把它清除掉。这时我们在上一步的基础上,需要补充的有这些:
- 记录待删除元素的待删除次数;
- 需要有两个值分别记录两个堆中的有效数据个数;
- 如何确保堆顶元素有效:
- 新增数据不会影响堆顶元素的有效性;
- 删除堆中数据可能影响到堆顶元素的有效性(包括删除窗口最左侧值、调整堆大小时的删除操作)。
【总结思路】 堆 + 哈希表
- 提供大小堆数据结构,并记录各堆中的有效数据个数;
- 延迟删除,记录待删除元素的待删除次数(哈希表);
- 要平衡大小堆,保证大小堆的有效元素个数关系;
- 保证堆顶元素有效性,在堆的的删除操作后清除无效的堆顶元素;
- 提供 添加元素、删除元素、获取中位数 的接口。
class MaxMinHeap {
priority_queue<int> maxHeap; // 大顶堆
priority_queue<int, vector<int>, greater<>> minHeap; // 小顶堆
unordered_map<int, int> delCountHash; // 哈希表 待删除次数
int n;
int maxHeapNums, minHeapNums; // 大小堆中的有效值数量
private:
// 调整大小堆元素比例
void adjust() {
if (maxHeapNums - minHeapNums > 1) { // 要从大顶堆取未删除的最大值放到小顶堆
minHeap.push(maxHeap.top());
maxHeap.pop();
++minHeapNums;
--maxHeapNums;
// 保证堆顶元素未删除
while (!maxHeap.empty() && delCountHash[maxHeap.top()] > 0) {
--delCountHash[maxHeap.top()];
maxHeap.pop();
}
} else if (maxHeapNums < minHeapNums) { // 从小顶堆中取未删除的最小值放到大顶堆
maxHeap.push(minHeap.top());
minHeap.pop();
++maxHeapNums;
--minHeapNums;
// 保证堆顶元素未删除
while (!minHeap.empty() && delCountHash[minHeap.top()] > 0) {
--delCountHash[minHeap.top()];
minHeap.pop();
}
}
}
public:
MaxMinHeap(int k) : n(k), maxHeapNums(0), minHeapNums(0) { }
// 添加元素
void add(int value) {
// 根据数据值 插入指定堆
if (maxHeap.empty() || maxHeap.top() >= value) {
maxHeap.push(value);
++maxHeapNums;
} else {
minHeap.push(value);
++minHeapNums;
}
// 调整大小堆元素比例
adjust();
}
// 删除元素
void del(int value) {
++delCountHash[value]; // 先记录下待删除
if (maxHeap.top() >= value) {
--maxHeapNums;
// 每次删除 保证堆顶元素不是待删除元素
while (!maxHeap.empty() && delCountHash[maxHeap.top()] > 0) {
--delCountHash[maxHeap.top()];
maxHeap.pop();
}
} else {
--minHeapNums;
// 每次删除 保证堆顶元素不是待删除元素
while (!minHeap.empty() && delCountHash[minHeap.top()] > 0) {
--delCountHash[minHeap.top()];
minHeap.pop();
}
}
// 调整大小堆元素比例
adjust();
}
// 获取中位数
double getMedian() {
if (n & 1)
return maxHeap.top();
else
return ((double)maxHeap.top() + minHeap.top()) / 2.0; // 两数相加可能造成越界 [2147483647,2147483647] 2
}
};
class Solution {
public:
vector<double> medianSlidingWindow(vector<int>& nums, int k) {
// 大小为 k 的滑动窗口
MaxMinHeap window(k);
for (int i = 0; i < k; ++i) {
window.add(nums[i]);
}
// 移动滑动窗口 [left, right)
vector<double> ans(1, window.getMedian());
for (int left = 0, right = k; right < nums.size(); ++left, ++right) {
window.add(nums[right]);
window.del(nums[left]);
ans.push_back(window.getMedian());
}
return ans;
}
};