剑指 Offer 41. 数据流中的中位数

108 阅读3分钟

「这是我参与2022首次更文挑战的第36天,活动详情查看:2022首次更文挑战

1、题目

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如:

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。

示例 1:

输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]

示例 2:

输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]

限制:

  • 最多会对 addNum、findMedian 进行 50000 次调用

2、知识补充

www.cnblogs.com/wangchaowei…

cloud.tencent.com/developer/a…

数据结构 - 堆

  • Heap是一种数据结构具有以下的特点: (1)完全二叉树; (2)heap中存储的值是偏序
  • Min-heap: 父节点的值小于或等于子节点的值;
  • Max-heap: 父节点的值大于或等于子节点的值;

image-20211113224306149.png

优先队列:

priority_queue称为“优先队列”,其底层是用堆实现。在优先队列中,队首元素一定是当前队列中优先级最高的哪一个。

默认的定义优先队列是大根堆,即父节点的值大于子节点的值。

获取堆顶元素

top():可以获得队首元素(堆顶元素),时间复杂度为O(1)。 与队列不一样的是,优先队列通过top()函数来访问队首元素(堆顶元素)。(队列是通过front()函数和back()函数访问下标)

入队

push(x) :令x入队,时间复杂度为O(logN),其中N为当前优先队列中的元素个数。

出队

pop(): 令队首元素(堆顶元素)出队,时间复杂度为O(logN),其中N为当前优先队列中的元素个数。

检测是否为空

empty():检测优先队列是否为空,返回true为空,false为非空。时间复杂度为O(1)

获取元素个数

size():用来获得优先队列中元素的个数,时间复杂度为 O(1)

案例代码

 #include
 #include
 using namespace std;
 int main(){
     priority_queue q;
 ​
     //入队
     q.push(3);
     q.push(4);
     q.push(1);
 ​
     //通过下标访问元素
     printf("%d\n",q.top());//输出4
 ​
     //出队
     q.pop();
     printf("%d\n",q.top());//输出3
 ​
     //检测队列是否为空
     if(q.empty() == true) {
         printf("Empty\n");
     } else {
         printf("Not Empty\n");
     }
 ​
     //获取长度
     //printf("%d\n",q.size());//输出3
 }

大根堆就是根节点是整棵树的最大值,小根堆就是根节点是整棵树的最小值。

基本数据类型的优先级设置

一般情况下,数字大的优先级更高。(char类型的为字典序最大) 对于基本结构的优先级设置。下面两种优先队列的定义是等价的:

 priority_queue<int> q;   // 大根堆
 priority_queue<int,vector<int>,less<int> > q;

如果想让优先队列总是把最小的元素放在队首,需进行以下定义:

 priority_queue<int, vector<int>, greater<int>> q //小根堆

3、思路

(双顶堆) O(logn)O(logn)

我们可以使用两个堆来解决这个问题,使用一个大顶堆保存整个数据流中较小的那一半元素,再使用一个小顶堆保存整个数据流中较大的那一半元素,同时大顶堆堆顶元素 <= 小顶堆堆顶的元素。

具体过程:

  • 1、建立一个大根堆,一个小根堆。大根堆存储小于当前中位数,小根堆存储大于等于当前中位数。
  • 2、执行addNum操作时,如果新加入的元素num小于等于大顶堆down堆顶元素,则加入大根堆中,否则加入小根堆中。
  • 3、为了维持左右两边数的数量,我们可以让大根堆的大小最多比小根堆大1,每次插入后,可能会导致数量不平衡,所以如果插入后哪边的元素过多了,我们将该堆的堆顶元素弹出插入到另一个堆中。
  • 4、当数据个数是奇数的时候,中位数就是大根堆堆顶,是偶数的时候就是大根堆与小根堆堆顶的平均值。

image-20211114213415034.png

4、c++代码

 class MedianFinder {
 public:
     priority_queue<int, vector<int>, greater<int>> up;  //小根堆
     priority_queue<int> down;  // 大根堆
 ​
     /** initialize your data structure here. */
     MedianFinder() {
 ​
     }
 ​
     void addNum(int num) {
         if (down.empty() || num <= down.top()) {
             down.push(num);
             if (down.size() > up.size() + 1) {
                 up.push(down.top());
                 down.pop();
             }
         } else {
             up.push(num);
             if (up.size() > down.size()) {
                 down.push(up.top());
                 up.pop();
             }
         }
     }
 ​
     double findMedian() {
         if ((down.size() + up.size()) % 2) return down.top();
         return (down.top() + up.top()) / 2.0;
     }
 };