滑动窗口中位数
中位数是有序序列最中间的那个数。如果序列的长度是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。
例如:
[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
以内的答案将被视作正确答案。
Sliding Window Median
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]
, the median is3
[2,3]
, the median is(2 + 3) / 2 = 2.5
Given an array nums, there is a sliding window of size k
which is moving from the very left of the array to the very right. You can only see the k
numbers 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.
For example,
Given nums = [1,3,-1,-3,5,3,6,7]
, and k = 3
.
Window 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
Therefore, return the median sliding window as [1,-1,-1,3,5,6]
Note:
- You may assume
k
is always valid, ie:k
is always smaller than input array’s size for non-empty array. - Answers within
10^-5
of the actual value will be accepted as correct.
解
方法:双向队列
我们首先要明白如下几点:
- 中位数要经过排序后再找
- 本题要求我们维护一个滑动窗口,即每次移动都要有元素的加入和退出
因此我们维护一个大根堆small
和一个小根堆large
,设当前元素有x
个, small
负责维护较小的部分,large
负责较大的部分,我们有以下引理:
引理1.
small
中的元素个数要么与large
中的元素个数相同,要么比large
中的元素个数多1个。
证明:small
中有
⌈
x
2
⌉
\lceil \frac{x}{2} \rceil
⌈2x⌉个元素,large
中有
⌊
x
2
⌋
\lfloor \frac{x}{2} \rfloor
⌊2x⌋个元素,易得引理1正确。
由此得到推论:
推论1. 当两个堆元素个数相等时,两堆顶元素平均值就是中位数,不相等时,
small
堆顶元素就是中位数。
由推论1我们就可以设计函数getMedian()
来求中位数。
同时由引理1我们还可以得到推论:
推论2. 当堆中成功加入一个元素,可能会出现不符合引理1的情况,即如下两种情况
small
元素比large
多2个small
元素比large
少1个
证明:当我们向堆中加入元素时要遵循以下规则:
- 如果
n
u
m
≤
t
o
p
num≤top
num≤top,我们就将其加入
small
中. - 如果
n
u
m
>
t
o
p
num>top
num>top,我们就将其加入
large
中
列举所有情况,就可以得到推论2.
由推论2我们可以设计insert(int num)
函数:对于第一种情况,我们将small
的堆顶元素放入large
;对于第二种情况,我们将large
的堆顶元素放入small
事实: 优先队列不支持移出非堆顶元素的操作。
因此我们需要使用延迟删除来解决移除元素的操作。延迟删除核心思想就是:记录要删除的元素,而不是真正的删除,当该元素出现在堆顶时再进行删除。
我们使用哈希表delayed
来实现延迟删除,键值对(num, cnt)
表示元素num
要被删除cnt
次。我们设计函数prune(T& heap)
实现该操作。当我们加入或者移除元素时,由于堆顶元素有可能改变因此我们需要调用prune(T& heap)
保证堆顶元素是不需要被删除的。
具体操作如下:
对于insert(int num)
,我们进行一定的修改:
- 当两个堆元素个数符合要求时,我们无需进行多余操作
small
元素比large
多2个时,我们将small
的堆顶元素放入large
,此时small
新的堆顶元素是可能需要被删除的,因此调用prune(T& heap)
。small
元素比large
少1个时,我们将large
的堆顶元素放入small
,此时large
新的堆顶元素是可能需要被删除的,因此调用prune(T& heap)
。
对于删除函数erase(int num)
,我们将delayed
中num
的键值cnt
加一即可。因为删除元素之后,两个堆的元素数有可能会不符合要求,因此需要进行类似insert(int num)
中的操作。
综上,我们可以写出以下代码:
class DualHeap {
private:
priority_queue<int> small;
priority_queue<int, vector<int>, greater<int>> large;
unordered_map<int, int> delayed;
int k;
int smallCount, largeCount;
template <typename T>
void prune(T& heap) {
while (!heap.empty()) {
int num = heap.top();
if (delayed.count(num)) {
// 延迟删除队列中有堆顶元素
--delayed[num];
if (!delayed[num]) {
delayed.erase(num);
}
heap.pop();
} else {
break;
}
}
}
void modify() {
if (smallCount > largeCount + 1) {
// small比large元素多2个
large.push(small.top());
small.pop();
--smallCount;
++largeCount;
// 因为small堆顶改变,需要检查新堆顶是否可以删除
prune(small);
} else if (smallCount < largeCount) {
// large比small元素多1个
small.push(large.top());
large.pop();
++smallCount;
--largeCount;
// 与上面同理
prune(large);
}
}
public:
DualHeap(int a) : k(a), smallCount(0), largeCount(0) {}
void insert(int num) {
if (small.empty() || num <= small.top()) {
small.push(num);
++smallCount;
} else {
large.push(num);
++largeCount;
}
modify();
}
void erase(int num) {
++delayed[num];
if (num <= small.top()) {
--smallCount;
if (num == small.top()) {
prune(small);
}
} else {
--largeCount;
if (num == large.top()) {
prune(large);
}
}
modify();
}
double getMedian() {
return k & 1 ? small.top() : ((double)small.top() + large.top()) / 2;
}
};
class Solution {
public:
vector<double> medianSlidingWindow(vector<int>& nums, int k) {
DualHeap dh(k);
for (int i = 0; i < k; ++i) {
dh.insert(nums[i]);
}
vector<double> ret = {dh.getMedian()};
for (int i = k; i < nums.size(); ++i) {
dh.insert(nums[i]);
dh.erase(nums[i - k]);
ret.push_back(dh.getMedian());
}
return ret;
}
};