题解 | #随时找到数据流的中位数#
随时找到数据流的中位数
http://www.nowcoder.com/practice/8c5e99acb1fa4bc3a342cd321100c0cd
题目描述
有一个源源不断的吐出整数的数据流,假设你有足够的空间来保存吐出的数。请设计一个名叫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(); } };
复杂度分析
- 时间复杂度:向大根堆和小根堆插入数据的时间复杂度为,查询中位数操作需要取出堆顶元素,复杂度为,时间复杂度符合要求。
- 空间复杂度:需要用堆存储个数,故复杂度为