题目描述
有一个源源不断的吐出整数的数据流,假设你有足够的空间来保存吐出的数。请设计一个名叫MedianHolder的结构,MedianHolder可以随时取得之前吐出所有数的中位数。
[要求]
1. 如果MedianHolder已经保存了吐出的N个数,那么将一个新数加入到MedianHolder的过程,其时间复杂度是O(logN)。
2. 取得已经吐出的N个数整体的中位数的过程,时间复杂度为O(1)
每行有一个整数opt表示操作类型
若opt=1,则接下来有一个整数N表示将N加入到结构中。
若opt=2,则表示询问当前所有数的中位数
方法一 暴力检索
解题思路
最简单的方法就是存储所有的整数,需要查询中位数时通过排序得到需要的结果。
代码示例(超时)
class Solution {
public:
/**
* the medians
* @param operations int整型vector<vector<>> ops
* @return double浮点型vector
*/
// 存储数据
vector<double> store;
vector<double> flowmedian(vector<vector<int> >& operations) {
// write code here
vector<double> res;
for(auto & v : operations) {
if(v[0] == 1)
addNum(v[1]);
else
res.push_back(findMedian());
}
return res;
}
void addNum(int num) {
// 朴素地把num加入vector中
store.push_back(num);
}
double findMedian() {
sort(store.begin(), store.end());
// 根据排序后的数组寻找中位数
int n = store.size();
// 边界情况
if(n == 0)
return -1;
return (n & 1 ? store[n / 2] : (store[n / 2 - 1] + store[n / 2]) * 0.5);
}
}; 复杂度分析
- 时间复杂度:插入操作需要
,查询中位数操作因为需要排序复杂度为
,所以时间复杂度为
。查询中位数时间复杂度远大于题目要求,故因超时未通过。
- 空间复杂度:需要存储
个数,故复杂度为
方法二 插入查找
解题思路
此方法为方法一的优化版本。方法一每次查询操作都对数组进行排序操作;本方法每次插入时保持数组的有序性,在查询时仅需要查询数组的中间位置一个或两个数即可。这样通过增加插入时的时间复杂度来减少查询时的时间复杂度。
代码示例
class Solution {
public:
/**
* the medians
* @param operations int整型vector<vector<>> ops
* @return double浮点型vector
*/
// 存储数据
vector<double> store;
vector<double> flowmedian(vector<vector<int> >& operations) {
// write code here
vector<double> res;
for(auto & v : operations) {
if(v[0] == 1)
addNum(v[1]);
else
res.push_back(findMedian());
}
return res;
}
void addNum(int num) {
// 通过二分法插入num,保证vector的有序性
if (store.empty())
store.push_back(num);
else
store.insert(lower_bound(store.begin(), store.end(), num), num);
}
double findMedian() {
// 因为数组有序,因此只需取出中间位置的数
int n = store.size();
// 边界情况
if(n == 0)
return -1;
return (n & 1 ? store[n / 2] : (store[n / 2 - 1] + store[n / 2]) * 0.5);
}
}; 复杂度分析
- 时间复杂度:插入操作时需要二分查找,复杂度为
,查询中位数操作只需要取出数组中间位置的数,复杂度为
,时间复杂度符合要求。
- 空间复杂度:需要存储
个数,故复杂度为
方法三 双堆
解题思路
根据题目要求的时间复杂度很容易想到树或者堆等数据结构。与方法二类似,我们在插入元素时保持数据有序,借助堆可以进一步优化时间复杂度(插入后的数据移动部分的时间复杂度)。
因为关心的是中位数的值,因此我们可以建立一个大顶堆lo和一个小顶堆hi,分别存储数据较小的一半和较大的一半,即
- lo的大小为N/2(N为偶数)或N+1/2(N为奇数)
- hi的大小为N/2(N为偶数)或N-1/2(N为奇数)
这样,我们可以根据lo和hi的堆顶元素找到中位数的值,即findMedian()函数的实现:
- lo的大小等于hi的大小时,N为偶数,中位数为lo和hi堆顶元素的平均值
- lo的大小大于hi的大小时,N为奇数,中位数为lo的堆顶元素
实现addNum()函数时,遵循以下规则:
- lo的大小等于hi的大小时,N为偶数,需向lo添加一个元素。实现方法:将新元素num插入至hi,再将hi堆顶元素插入至lo
- lo的大小大于hi的大小时,N为奇数,需向hi添加一个元素。实现方法:将新元素num插入至lo,再将llo堆顶元素插入至hi
这样,可以保证hi中所有元素大于lo中所有元素,以保证中位数的查询。
代码示例
class Solution {
public:
/**
* the medians
* @param operations int整型vector<vector<>> ops
* @return double浮点型vector
*/
// 大顶堆,存储较小的那一半数
priority_queue<int> lo;
// 小顶堆,存储较大的那一半数
priority_queue<int, vector<int>, greater<int>> hi;
vector<double> flowmedian(vector<vector<int> >& operations) {
// write code here
vector<double> res;
for(auto & v : operations) {
if(v[0] == 1)
addNum(v[1]);
else
res.push_back(findMedian());
}
return res;
}
void addNum(int num) {
// 此时需要将num添加入hi中以保证堆大小的平衡
if (lo.size() > hi.size()) {
// 保证插入到hi中的元素大于lo中所有元素
lo.emplace(num);
hi.emplace(lo.top());
lo.pop();
}
// 此时需要将num添加入lo中以保证堆大小的平衡
else {
// 保证插入到lo中的元素小于hi中所有元素
hi.emplace(num);
lo.emplace(hi.top());
hi.pop();
}
}
double findMedian() {
// 边界情况
if(lo.size() == 0)
return -1;
// 根据堆顶元素得到中位数
if(hi.size() == lo.size())
return (hi.top() + lo.top()) / 2.0;
else
return lo.top();
}
}; 复杂度分析
- 时间复杂度:向大根堆和小根堆插入数据的时间复杂度为
,查询中位数操作需要取出堆顶元素,复杂度为
,时间复杂度符合要求。
- 空间复杂度:需要用堆存储
个数,故复杂度为



京公网安备 11010502036488号