题目
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
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 以内的答案将被接受。
示例1:
输入
[“MedianFinder”, “addNum”, “addNum”, “findMedian”, “addNum”, “findMedian”]
[[], [1], [2], [], [3], []]
输出
[null, null, null, 1.5, null, 2.0]
这里输入的两个列表,第一个列表表示调用的Method名,调用的Method的对应位置于第二个列表中,表示该Method的参数;
输出的列表,不用多解释,为对应输入位置调用Method的输出结果;
提示:
-10^5 <= num <= 10^5
- 在调用
findMedian
之前,数据结构中至少有一个元素 - 最多
5 * 10^4
次调用addNum
和findMedian
思路
两个优先队列或者两个堆,前半段存大一些的数,后半段存小一些的数,比如:
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
前半段:10, 9, 8, 7, 6
后半段:5, 4, 3, 2, 1
当然优先队列和堆的特性,只能反应最大值或最小值在堆头这个节点,前半段这题以小顶堆存储,堆顶是6;后半段是大顶堆存储,堆顶是5;
堆结构上可以看作是一个二叉树,存储上是连续的数组形式(至少手写堆是)
堆顶下面同层的节点,没有谁一定比谁大或小之说,比如第二层的两个节点,他们是堆顶这个根节点的左节点和右节点,并不能确定这个位置谁一定大,谁一定小;这个位置随着不停地更换,因为add值和delete值的缘故,左节点的位置的值,可能比右节点的值大,也可能小;
唯一能确定的是,这个节点,比他下面的所有子孙节点大或小;大顶堆,这个节点比他下面的所有子孙节点大;小顶堆,这个节点比他下面的所有子孙节点小;
findMedian
查找时,
存储的总数为偶数时,取前半段的最小值,和后半段的最大值,相加除以2;
存储的总数为奇数时,取后半段的最大值,即为中位数
大顶堆的顶部,是存储的这组数的最大值;大顶堆用作这题的后半段数据存储;
小顶堆的顶部,是存储的这组数的最小值;小顶堆用作这题的前半段数据存储;
(使用优先队列priority_queue是一模一样的,后面直接在不同的实现方式上贴代码)
addNum
添加时,
加进来前,已存储的总数为偶数时,
先跟前半段的小顶堆比较,堆顶和新加的num,总有一个需要加到后半段;
加进来前,已存储的总数为奇数时,
先跟后半段的大顶堆比较,堆顶和新加的num,总有一个需要加到前半段;
按照这个逻辑,可以在findMedian
时,找到中位数;
当然,在加进来num时,总数为0,也就是第一次加入num时,我们直接将其加入到后半段的堆货有限队列中;
实现方式
堆
这个时间效率是比后两种方式更快的
#define MAXHEAP 50002
// 最多调用50000次addNum和findMedian,假设都是addNum,最多存储int heap[50000]
// 分为两个堆,我自己每次都是从heap[1]开始存,这样heap[idx],idx>>1就能直接得到左节点
// 且位运算是块于+ -运算的
// 前后堆最多25000, 25001个数,都是从heap[1]开始存,空间就需要25001,25002个int
// 实际申请的时候,我是直接50002/2,因为这是查找中位数的题,肯定至少有一次findMedian的调用,所以后半段少申请一位没关系
// 细纠结这个heap大小没必要,但是纠结完了,我神清气爽^0^
inline bool CmpForSmallHeap(int a, int b) {
return a < b;
}
inline bool CmpForBigHeap(int a, int b) {
return b < a;
}
struct Heap {
int N;
bool (*Cmp)(int, int);
int heap[MAXHEAP/2];
void init(int preOrBack) {
N = 0;
// 0, pre -> small heap; 1, back -> big heap
Cmp = preOrBack ? &CmpForBigHeap : &CmpForSmallHeap;
}
void swap(int idxa, int idxb) {
int temp = heap[idxa];
heap[idxa] = heap[idxb];
heap[idxb] = temp;
}
void up(int idx){
while (idx > 1 && Cmp(heap[idx], heap[idx >> 1])) {
swap(idx, idx>>1);
idx >>= 1;
}
}
void down(int idx){
int child;
while ((child = idx << 1) <= N) {
if (child < N && Cmp(heap[child + 1], heap[child])) child++;
if (Cmp(heap[child], heap[idx])) {
swap(child, idx);
idx = child;
}
else return;
}
}
void add(int one){
heap[++N] = one;
up(N);
}
int pop() {
int temp = heap[1];
swap(1, N--);
down(1);
return temp;
}
};
class MedianFinder {
public:
Heap ph, bh; // pre heap; back heap;
public:
MedianFinder() {
this->ph.init(0);
this->bh.init(1);
}
void addNum(int num) {
// 加进来前,总数:
// 奇 -> 跟bh[1]比,谁上去
// 偶 -> 跟ph[1]比,谁下去
int total = ph.N + bh.N;
if (total == 0) bh.add(num);
else {
if (total % 2 != 0) {
if (num >= bh.heap[1]) {
ph.add(num);
}
else {
ph.add(bh.pop());
bh.add(num);
}
} // if (total % 2 == 0)
else {
if (num < ph.heap[1]) {
bh.add(num);
}
else {
bh.add(ph.pop());
ph.add(num);
}
}
}
}
double findMedian() {
int total = ph.N + bh.N;
if (total % 2 == 0) {
return (ph.heap[1] + bh.heap[1]) / 2.0;
}
else {
return bh.heap[1];
}
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
后两种实现方式是我看了官方题解,是同样的思路,用的库的方式(queue头文件的priority_queue, set头文件的multiset)实现的,我以自己的方式写了一遍,但也是没什么大的差别;
库的优点是代码量少,我们只要注重逻辑的算法就可以了;
优先队列
#include <queue>
#include <iostream>
using namespace std;
class MedianFinder {
public:
priority_queue<int, vector<int>, greater<int>> ph; // pre heap
priority_queue<int, vector<int>> bh; // back heap
public:
MedianFinder() {
}
void addNum(int num) {
int total = ph.size() + bh.size();
if (total == 0) bh.push(num);
else {
if (total % 2 != 0) {
if (num >= bh.top()) {
ph.push(num);
}
else {
ph.push(bh.top()); bh.pop();
bh.push(num);
}
} // total % 2 == 0
else {
if (ph.top() >= num) {
bh.push(num);
}
else {
bh.push(ph.top()); ph.pop();
ph.push(num);
}
}
}
}
double findMedian() {
int total = ph.size() + bh.size();
if (total % 2 != 0) {
return bh.top();
}
else {
return (ph.top() + bh.top()) / 2.0;
}
}
};
multiset容器
multiset是可以存储重复元素,自动排序的;
set也可以自动排序,但是不能存储值相同的元素;
所以这题multiset容器适合,
因为涉及排序,效率也是这三种方式,时间效率较慢的一个;
#include <set>
class MedianFinder {
public:
multiset<int, less<int>> ph;
multiset<int, greater<int>> bh;
public:
MedianFinder() {
}
void addNum(int num) {
int total = ph.size() + bh.size();
if (total == 0) bh.insert(num);
else {
if (total % 2 != 0) {
if (num >= *bh.begin()) {
ph.insert(num);
}
else {
ph.insert(*bh.begin()); bh.erase(bh.begin());
bh.insert(num);
}
} // if total % 2 == 0
else {
if (num <= *ph.begin()) { // !!!bh.begin() -> ph.begin()
bh.insert(num);
}
else {
bh.insert(*ph.begin()); ph.erase(ph.begin()); //bh.insert(*bh.begin()); -> bh.insert(*ph.begin());
ph.insert(num);
}
}
}
}
double findMedian() {
int total = ph.size() + bh.size();
if (total % 2 == 0) {
return (*ph.begin() + *bh.begin()) / 2.0;
}
else {
return *bh.begin();
}
}
};