一、题目:
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
二、例子:
- 例如
arr = [2,3,4]
的中位数是3
。- 例如
arr = [2,3]
的中位数是(2 + 3) / 2 = 2.5
三、实现 MedianFinder 类:
MedianFinder()
初始化MedianFinder
对象。
void addNum(int num)
将数据流中的整数num
添加到数据结构中。
double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差10-5
以内的答案将被接受。
四、分析:
由题可知,在一个有序的整数列表中,如果该列表的大小是偶数,则取其两个中位数的平均值;如果该列表的大小是奇数,则取其中位数。由于没有说不可以插入相同值,所以需要一个方法能够使其插入有序的同时,支持插入相同值。取其中位数,有的同学说这不简单,直接取列表大小的中位下标即可轻松得到答案,但事实是这样吗?
请看如下代码:
typedef struct {
int *data; //存储数据
int n; //列表大小
} MedianFinder;
void xun(MedianFinder *obj){ //冒泡排序
for(int i=0;i<obj->n;i++){
for(int j=0;j<obj->n-1;j++){
if(obj->data[j+1]<obj->data[j]){
int a=obj->data[j+1];
int b=obj->data[j];
a=a^b;
b=a^b;
a=a^b;
}
}
}
return;
}
MedianFinder* medianFinderCreate() { //初始化
MedianFinder *m=(MedianFinder *)malloc(sizeof(MedianFinder));
m->data=(int *)malloc(sizeof(int)*400);
m->n=0;
return m;
}
void medianFinderAddNum(MedianFinder* obj, int num) { //插入数据
obj->data[obj->n]=num;
obj->n+=1;
return;
}
double medianFinderFindMedian(MedianFinder* obj) { //取中位数
if(obj==NULL)return 0;
xun(obj);
int a=obj->n;
if(obj->n%2==1){
return obj->data[a/2];
}else{
return (double)(obj->data[a/2-1]+obj->data[a/2])/2; //从0开始
}
}
void medianFinderFree(MedianFinder* obj) { //释放空间
if(obj==NULL)return;
free(obj->data);
free(obj);
return;
}
运行如下:
提交后:
错误总结:
这种思路对于一些简单的测试用例来说,是可以的。但是,对于一些特殊和并不常见的测试用例来说,这种方法是行不通的。
如:
输入:
["MedianFinder","addNum","findMedian","addNum","findMedian","addNum","findMedian",
"addNum","findMedian","addNum","findMedian","addNum","findMedian","addNum",
"findMedian","addNum","findMedian","addNum","findMedian","addNum","findMedian",
"addNum","findMedian"]
[[],[6],[],[10],[],[2],[],[6],[],[5],[],[0],[],[6],[],[3],[],[1],[],[0],[],[0],[]]
五、使用对顶堆来解决:
(1)什么是对顶堆:
简单来说,就是在一个列表结构中,该列即具有小顶堆的性质,也具有大顶堆的性质。对顶堆有以下特性:
完全二叉树结构:对顶堆是一棵完全二叉树,即除了最底层外,其他层节点都是满的,最底层的节点从左到右连续排列。
有序性质:对顶堆中的每个节点,父节点的值不小于(或不大于)子节点的值。这种有序性质可以分为两种类型:最小堆和最大堆。
- 最小堆:父节点的值小于等于子节点的值,即根节点的值最小。
- 最大堆:父节点的值大于等于子节点的值,即根节点的值最大。
用途:
对顶堆常用于实现优先队列,能够快速找到最小(或最大)值,并具有高效的插入和删除操作。
(2)如图所示:
假如上图中,浅绿色的位置是中位数,其前面(包括自己)是一个大顶堆,其后面是一个小顶堆,拥有这种相似结构的列,也称为对顶堆结构。
思路:假如有一个对顶堆后,如果为空,先向前面的小顶堆插入数据,而后根据要插入的数据的大小与第一个插入的数据相比。如果大了,插入后面,否则插入前面。完成插入操作后,需要检查两个堆的大小,要保持前一个堆为n+1个有序数据,后一个堆为n个有序数据。因为这样子,当列表大小为奇数时,直接返回头部元素即是答案;否则,就将两个堆的头部元素弹出,相加后取平均值即是答案。
六、代码:
class MedianFinder {
public:
typedef pair<int,int>pir; //pair用来实现相同数据插入
set<pir>s1; //set本为小顶堆,有序
set<pir>s2; //模拟大顶堆,需要将数的符号取反
int tot; //npc
MedianFinder() {
this->tot=0; //npc
}
void addNum(int num) {
if(s1.size()==0||num<-s1.begin()->first){ //如果堆为空或插入的数小于第一个数
s1.insert(pir(-num,tot++)); //插入小顶堆
}else{
s2.insert(pir(num,tot++)); //插入大顶堆
}
int n1=(s1.size()+s2.size()+1)/2; //堆中有n+1个数
if(s1.size()==n1)return; //如果相等,返回
if(s1.size()<n1){ //如果小于n+1
s1.insert(pir(-s2.begin()->first,tot++)); //则将s2中的头部数据取反,再插入s1
s2.erase(s2.begin()); //弹出
}else{
s2.insert(pir(-s1.begin()->first,tot++)); //否则将s1中的头部数据取反,再插入s2
s1.erase(s1.begin()); //弹出
}
return;
}
double findMedian() {
if((s1.size()+s2.size())%2){ //如果为奇数
return -s1.begin()->first; //直接取反返回头部元素
}
double a=-s1.begin()->first;
double b=s2.begin()->first;
return (a+b)/2; //否则取两个头部元素相加之后再取平均值
}
};
输出结果:
文章到此结束!