Leetcode:数据流的中位数
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
例如,
[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 范围内,你将如何优化你的算法?
思路一:全部排序取中间数
插入数据即对所有数据进行重新排序,开销非常大。
思路二:大顶堆+小顶堆
由于只需要中间两个数,可以将数据分成两半,前半比后半小,且前半建立大顶堆,后半建立小顶堆。保持前堆元素个数大于或者等于后堆元素个数的约束条件。
新插入一个元素时共有4种情况:
第一种是直接加入前堆,第二种是直接加入后堆,第三种是前堆堆顶加入后堆,新元素替换前堆堆顶,第四种是后堆堆顶元素加入前堆,新元素替换后堆堆顶。
4种操作的条件大家也能枚举出来。
需要注意的有一点,堆中如果是插入元素,需要重新建堆。如果是将堆顶元素替换成新元素,则直接调整堆就可以。
建堆操作:分别以堆的中间位置元素开始到第一个元素为起始点,进行堆调整操作。
调整堆操作:以某个元素为起始点,交换与左右孩子的位置并递归直到叶节点或者与左右孩子节点满足堆条件。
代码如下:
class MedianFinder {
vector<int> foreVec,postVec;
public:
/** initialize your data structure here. */
MedianFinder() {
}
void addNum(int num) {
if(foreVec.empty()){
foreVec.push_back(num);
}else if(postVec.empty()){
if(num>=foreVec.front()) postVec.push_back(num);
else{
postVec.push_back(foreVec.front());
foreVec.front()=num;
}
}else{
if(foreVec.size()==postVec.size()){
if((num>foreVec.front() && num<postVec.front()) || (num<=foreVec.front())){
foreVec.push_back(num);
buildHeap(foreVec,true);
}else if(num>=postVec.front()){
foreVec.push_back(postVec.front());
buildHeap(foreVec,true);
postVec.front()=num;
adjustHeap(postVec,0,false);
}
}else{
assert(foreVec.size()>postVec.size());
if(num>=foreVec.front()){
postVec.push_back(num);
buildHeap(postVec,false);
}else{
postVec.push_back(foreVec.front());
buildHeap(postVec,false);
foreVec.front()=num;
adjustHeap(foreVec,0,true);
}
}
}
}
double findMedian() {
double res=0.0;
if(foreVec.size()==postVec.size()){
res+=foreVec.front();
res+=postVec.front();
res/=2;
}else{
res=foreVec.front();
}
return res;
}
void buildHeap(vector<int>& v,bool bigTop){
for(int i=v.size()/2;i>=0;--i){
adjustHeap(v,i,bigTop);
}
}
void adjustHeap(vector<int>& v,int start,bool bigTop){
int len=v.size();
int i=start;
auto iter=v.begin();
while(i<len && 2*i+1<len){
int left=2*i+1;
int right=2*i+2;
int *pBigger=&left;
if((bigTop && right<len && *(iter+right)>*(iter+left)) ||(!bigTop && right<len && *(iter+right)<*(iter+left))){
pBigger=&right;
}
if((bigTop && *(iter+i)<*(iter+*pBigger)) || (!bigTop && *(iter+i)>*(iter+*pBigger))){
swap(*(iter+i),*(iter+*pBigger));
i=*pBigger;
}else{
break;
}
}
}
};
关于进阶
1。如果数据流中所有整数都在 0 到 100 范围内,你将如何优化你的算法?
不用建堆了,用一个int[101] arr记录某个数据出现的次数。要取中位数时,第一次遍历数组累加得出总个数。然后除以2得出要找的数的位置p并知道是奇数还是偶数,然后第二次遍历数组,p-=arr[i],当p变为1和0时分别记下i、j。奇数个数则返回j,偶数个数则返回(i+j)/2。
2。如果数据流中 99% 的整数都在 0 到 100 范围内,你将如何优化你的算法?
同样参照1的思路,用int[102] arr记录数据出现的次数,arr[101]记录大于100的数的次数。由于数据主要分布在100及以内,可知中间的2个数还是在100或以内,1的算法仍然有效。