一、题目介绍
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
示例:
addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3)
findMedian() -> 2
进阶:
如果数据流中所有整数都在 0 到 100 范围内,你将如何优化你的算法?
如果数据流中 99% 的整数都在 0 到 100 范围内,你将如何优化你的算法?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-median-from-data-stream
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
二、解题思路
按照题目要求,最简单的方法是维持一个动态数组,在每次执行插入操作时将元素num保存到动态数组中;在每次findMedian时先将数组排序,再查找中位数返回结果。该方法超时,本文不进行介绍。下面介绍其他两种解决方法:
(1)两个堆的方法,即维护一个最大堆和一个最小堆,最大堆中保存小于等于中位数的元素,最小堆中保存大于中位数的元素。如果数组的长度n为奇数时,最大堆的堆顶即为结果;如果n为偶数,最大堆和最小堆的堆顶元素的平均值即为结果。该方法的难点在于怎么平衡两个堆中元素的个数,平衡方法请见代码。
(2)Multiset 和双指针,利用multiset(内部的数据结构为自平衡二进制搜索树)自动排序的功能。我们保持两个指针:一个用于中位数较低的元素,另一个用于中位数较高的元素。当元素总数为奇数时,两个指针都指向同一个中值元素(因为在本例中只有一个中值)。当元素数为偶数时,指针指向两个连续的元素,其平均值是输入的代表中位数。其中两个指针的更新方式,请见代码。
三、解题代码
(1)两个堆的方法
class MedianFinder {
private:
priority_queue<int> low; //最大堆 保存小于等于中位数的元素
priority_queue<int, vector<int>, greater<int>> high; //最小堆 保存大于中位数的元素
public:
/** initialize your data structure here. */
MedianFinder() {
}
void addNum(int num) {
low.push(num); //元素首先存入到最大堆中
high.push(low.top()); //将最大堆中堆顶元素保存到最小堆中
low.pop();
if(low.size() < high.size()) //要保持最大堆元素的个数大于等于最小堆的元素,保持平衡
{
low.push(high.top());
high.pop();
}
}
double findMedian() {
//如果元素为2*n + 1个,则最大堆中存入n+1个,最小堆中存入n个
//如果元素为2*n,则最大堆中存入n个,最小堆存入n个
return low.size() > high.size() ? low.top() : (low.top() + high.top()) * 0.5;
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
(2)multiset + 双指针方法
class MedianFinder {
private:
multiset<int> data;
multiset<int>::iterator l_mid, r_mid;
public:
/** initialize your data structure here. */
MedianFinder():l_mid(data.end()),
r_mid(data.end()) {
}
void addNum(int num) {
int n = data.size();
data.insert(num);
if(n == 0) //如果哈希集合为空,两个指针指向同一个元素
{
l_mid = data.begin();
r_mid = data.begin();
}
else if(n & 1) //n为奇数时,插入num之后变为偶数
{
if(num < *l_mid) //num插入到小于中位数的一边
{
l_mid--;
}
else //num插入到大于等于中位数的一边
{
r_mid++;
}
}
else //n为偶数时,插入num之后变为奇数
{
if(num > *l_mid && num < *r_mid) //num的值介于两个中位数之间
{
l_mid++;
r_mid--;
}
else if(num >= *r_mid) //num插入到大于等于右中位数的一边
{
l_mid++;
}
else // num <= *l_mid < *r_mid 注意当num == *l_mid时,num插入到l_mid和r_mid的中间
{
l_mid = --r_mid; //保持*l_mid不变且更新了r_mid所指向的元素
}
}
}
double findMedian() {
return (*l_mid + *r_mid) * 0.5;
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
四、解题结果
(1)两个堆的方法
(2)multiset + 双指针方法