一、题目
LeetCode 295.Find Median from Data Stream
难度:hard
设计一个数据结构,动态维护一组数据,有两个功能
1、支持添加元素
2、能够得到这个序列的中位数
中位数定义:
#1.如果元素个数是偶数,返回已排序序列最中间的两个数字的平均数
#2.如果元素个数是奇数,返回已排序序列中间那个数
二、分析
能够看到该题的标记是hard,显然凭自己的脑子瞎想肯定没用,一定用到了一些trick
首先该题用到了堆的思想
我们可以把整个序列从中间一分为二,分成两个堆
左边的序列组成"最大堆",右边的序列组成"最小堆"
最大堆能够得到左边序列的最大值,最小堆能够得到右边序列的最小值
那么只要保证两个堆元素个数一样多,或者一个堆比另一个堆多一个元素
这时候,两个堆的堆顶元素,就是我们需要求中位数时,要用到的元素
经过分析上述分析,我们大致可以知道题目的解法
还要考虑一个问题
添加元素时,怎么维持两个堆元素数量的"平衡"?
基本想法就是我们两个堆,谁元素少,谁优先获得元素数目的提升
可以先看算法,在算法后面,再具体介绍其间的逻辑,回头再看上述的表述,会很亲切
三、用到的工具
堆
在c++中,用到堆,就要引用队列
#include <queue>
因为堆,在这里称为优先级队列
默认创建的堆是最大堆
最大堆的创建方式如下:(默认方式)
priority_queue< int > big_heap;
也可这样创建:
priority_queue< <int>,vector<int>,less<int> > big_heap;
最小堆创建方式如下:
priority< <int>,vector<int>,greater<int> > small_heap;
四、代码
1、main方法
main方法中,创建一个数组
遍历数组,每次添加 当前访问的数字 到 我们解题的数据结构中
接着查看我们的当前情况下的中位数
int main(){
Solution solve;
int test[] = {1,3,5,2,3,6,7,4,33,22,16};
for(int i = 0;i < sizeof(test)/sizeof(test[0]);i++){
solve.addNum(test[i]);
printf(" 当前数组的中位数是%lf\n",solve.findMedium());
}
}
可以看到代码的第四行中,用到了一段比较复杂的描述数组大小的表达式
sizeof(test)/sizeof(test[0])
c++中没有返回数组大小的方法!!!
可以看到我们数组的大小是11
sizeof(test) 返回的是44
sizeof(test[0])返回的是4,是每个元素的大小,通过这样的方式来表述数组的大小
通过main()方法,我们知道,数据结构有两个主要方法
#1.addNum()
添加一个数到结构中
#2.findMedium()
返回中位数
2、解题用的类 Solution
大致框架是:
两个堆,内部引用
两个方法,一个添加元素,一个返回中位数
struct Solution{
private:
priority_queue<double> big_heap;
priority_queue<double,vector<double>,greater<double> > small_heap;
public:
void addNum(int num){
}
double findMedium(){
}
};
#1.findMedium()找中位数方法
先来看查找中位数的方法
我们的思路就是找到两个堆的堆顶,两个堆元素个数要么相同,要么相差1
左边是最大堆,右边是最小堆
左边的最大堆的堆顶保存着左边一半有序序列的最大值
右边的最大堆的堆顶保存着右边一半有序序列的最小值
这两个堆的堆顶就保存着整个序列最中间的两个元素
考虑两种情况
#1.两个堆大小相同
那么中位数就是两个堆堆顶的平局数
#2.两个堆大小不同
那么中位数就是那个 规模略大的堆 的 堆顶元素
double findMedium(){
if(big_heap.size() == small_heap.size()) //两个堆尺寸相同
return (big_heap.top()+small_heap.top())/2;
else if(big_heap.size() > small_heap.size()) //左边的最大堆尺寸大
return big_heap.top();
else
return small_heap.top(); //右边的最小堆尺寸大
}
#2.addNum(int num)添加元素方法
这里首先考虑添加第一个元素,第一个元素随便放在哪个堆
这里就放在左边堆里
然后按照两个堆的尺寸,分成三种情况考虑
void addNum(int num){
//1.考虑第一次加,往大堆中先填上第一个数字
if(big_heap.empty()){
big_heap.push(num);
return;
}
//开始考虑左右两个堆的尺寸,基本思路是两个堆谁少给谁添,一样给大堆添
//左边堆尺寸大,给右边添加元素,使两堆尺寸相等(和左边堆顶比较)
if(big_heap.size() > small_heap.size()){
if(num > big_heap.size())
small_heap.push(num);
else{
small_heap.push(big_heap.top());
big_heap.pop();
big_heap.push(num);
}
}
//两边堆尺寸相同
else if(big_heap.size() == small_heap.size()){
if(num < big_heap.top())
big_heap.push(num);
else
small_heap.push(num);
}
//左边堆尺寸小,给左边添加元素,使两堆尺寸相等(和右边堆堆顶比较)
else if(big_heap.size() < small_heap.size()){
if(num < small_heap.top()){
big_heap.push(num);
}
else{
big_heap.push(small_heap.top());
small_heap.pop();
small_heap.push(num);
}
}
}
这里通过一段实例,再来看上述的代码
如程序中给的 1,3,5,2,3序列
##1.两个堆空,执行上方第一种情况,左边的最大堆添加1
##2.左边堆尺寸大,执行上方第二种情况,现在需要给右边添加元素
那么现在是左边堆堆顶1 右边堆堆顶3
##3.两个堆尺寸相同,执行上方第三种情况
那么现在左边堆堆顶1,右边堆有元素3,5,堆顶3(最小堆)
##4.添加2,右边堆尺寸大,执行上方第四种情况,现在要给左边添加元素
那么按照程序,左边堆有元素1,2 堆顶为2 右边堆有元素3,5 堆顶为3
基本情况就是这么个情况
附上所有代码:
#include <stdio.h>
#include <queue>
using namespace std;
class Solution{
private:
priority_queue<double> big_heap;
priority_queue<double,vector<double>,greater<double> > small_heap;
public:
void addNum(int num){
//1.考虑第一次加,往大堆中先填上第一个数字
if(big_heap.empty()){
big_heap.push(num);
return;
}
//开始考虑左右两个堆的尺寸,基本思路是两个堆谁少给谁添,一样给大堆添
//左边堆尺寸大,给右边添加元素,使两堆尺寸相等(和左边堆顶比较)
if(big_heap.size() > small_heap.size()){
if(num > big_heap.size())
small_heap.push(num);
else{
small_heap.push(big_heap.top());
big_heap.pop();
big_heap.push(num);
}
}
//两边堆尺寸相同
else if(big_heap.size() == small_heap.size()){
if(num < big_heap.top())
big_heap.push(num);
else
small_heap.push(num);
}
//左边堆尺寸小,给左边添加元素,使两堆尺寸相等(和右边堆堆顶比较)
else if(big_heap.size() < small_heap.size()){
if(num < small_heap.top()){
big_heap.push(num);
}
else{
big_heap.push(small_heap.top());
small_heap.pop();
small_heap.push(num);
}
}
}
double findMedium(){
if(big_heap.size() == small_heap.size()) //两个堆尺寸相同
return (big_heap.top()+small_heap.top())/2;
else if(big_heap.size() > small_heap.size()) //左边的最大堆尺寸大
return big_heap.top();
else
return small_heap.top(); //右边的最小堆尺寸大
}
};
int main(){
Solution solve;
int test[] = {1,3,5,2,3,6,7,4,33,22,16};
for(int i = 0;i < sizeof(test)/sizeof(test[0]);i++){
solve.addNum(test[i]);
printf(" 当前数组的中位数是%lf\n",solve.findMedium());
}
}
五、回头分析
通过上面的分析,我们知道,得到中位数,就是要得到两个堆的堆顶元素
而关键是怎么动态地维护这两个堆,也就是怎么添加元素
这里最重要的是,添加元素时,"当前待添加元素"要和哪个堆的元素进行比较?
如果两个堆大小相等,那么无所谓,程序中待添加元素是和左边堆堆顶元素进行比较
元素如果小于左边堆堆顶,自然,这属于左边那个堆,没有问题
因为左边那个堆是最大堆,最大堆堆顶存储了左边这个序列的最大值
这个元素比左边序列最大值小,自然属于左边这个堆,“很公平”
但是假设左边堆堆顶是元素7 右边堆堆顶是元素11
那么 8,9,10这三个元素如果出现就被添加到右边堆了
左边堆就失去对中间这些元素的控制权了,算是"比较吃亏"的,但是我们两个堆大小相等,也就没什么好计较的
如果左边堆尺寸大
那么我们的目的是让右边堆的尺寸加1
这时候,左边堆尽量要让着右边堆,意思就是放弃中间这几个数字的控制权
在代码中的体现就是数字和左边堆的堆顶元素比较,这样遇到中间这几个数字,都给右边堆
如果出现两个堆堆顶元素之间的数字,一律给右边堆吃
实在没办法,遇到了元素就应该放在左边堆的情况,那么就把左边堆的大哥,堆顶元素给右边
然后新元素添加进来
如果右边堆尺寸大
和上面的逻辑相似
目的是让左边堆尺寸加1
右边堆放弃中间这几个数字的控制权
在代码中的体现就是数字和右边堆的堆顶元素比较
六、小结
现在是凌晨3点,睡不着觉,把今天这个题整理一下
小小的题目里蕴含着丰富的逻辑和思想,有趣有趣
你是猪吗??
是的,我是猪!
你他妈还睡不睡?
不太想睡怎么办?