从数据流中获取中位数

  • 需求描述
  • 需求分析
    • C++代码如下
    • python代码

需求描述

  有一个动态的数据流,如何比较快的获得数据流的中位数。这个过程中,数据流可能会有新的数据加入。中位数定义为元素个数为奇数的序列的排序结果中间位置元素值,偶数数列的排序结果中间位置的两个元素的元素值的平均。

需求分析

  首先要获得数据流的中位数,这个问题可以轻易转换成查找序列中的第k大的数,如果序列长度为偶数,则要查找两次,但是不会影响复杂度。但是现在还要处理的一个问题是,这个数据流的元素个数会增加的,元素一旦增加,很可能中位数就变了,如果再要获得就不会很方便。我们采用这个办法的话,就得重新查找中位数。
  总结来看,我们查找一次中位数的时间复杂度是O(n)O(n)O(n),分析在链接中的文章写得很清楚了。维护这样的数据流,每新来一个数据插入数据的时间复杂度就是O(1)O(1)O(1)了。因为要使用partition函数,所以这个数据流需要是顺序表的结构。
  可能要多次查找中位数,我们维持一个排序的序列,要查找中位数只需要O(1)O(1)O(1)的时间复杂度。因为如果是顺序表的话,直接随机存储访问中间元素即可,如果是链表,我们需要设置两个指针,来指向中间元素,插入元素后这两个指针最多向后移动一个元素,不带来额外的复杂度。但是使用排序的线性结构,新插入的元素直接插入会破坏排序结构,所以只能找好位置再插入,在排序的线性结构中插入一个元素,并继续维持有序,插入的时间复杂度就是O(n)O(n)O(n),分析可以参考插入排序(可见排序算法的重要性了)。这种方式可以通过顺序表或者链表实现。
  完成这个问题,可以把问题分成两部分,那就是查找中位数,和插入数据。继续检索其他数据结构来完成这两个任务,平衡二叉树(平衡二叉树一定是二叉搜索树)可以在O(1)O(1)O(1)的时间插入元素,在O(lgn)O(lgn)O(lgn)的时间内查找指定元素。但是这里显然,我们不确定中位数,没法直接查找,我们可以思考如何对数据结构进行扩充来实现这种操作。
  平衡二叉树的定义是自己以及所有子树的 左右子树的高度差是{-1,0,1}的二叉搜索树,《剑指offer》 中提到了通过扩充平衡二叉树,将定义改为任意子树的左右子树节点数差是{-1,0,1}的二叉搜索树。经过我的仔细分析,这个是无法实现的。因为我们所知的平衡二叉树,因为插入节导致的不平衡状态总结起来有四种,分别是我们所知道的LL, LR, RR, RL型不平衡。但是修改定义之后的平衡二叉树因为插入节点而导致的不平衡状态有无数种。平衡二叉树满足很好的递归性质,调整不平衡的时候是不需要上溯的,按照对应情况进行旋转即可。但是修改定义之后的二叉树不平衡调整是需要上溯的。以上两点理由决定了这种扩充无法实现(这是我个人的分析,欢迎讨论)。
  树虽然性质很优,但是很难用来查找中位数。
  我个人的思考就是继续使用平衡二叉树,但是给数的节点保存一个额外的字段,就是孩子节点的总数。插入节点的时候在哪个子树插入,就把插入位置的查找路径上每个节点的孩子节点数+1。如果遇到因为插入导致的不平衡状态,在平衡旋转的时候也要做特殊处理,这个子节点数目调整很容易实现。以LL型平衡旋转为例,左子节点顶替了父亲的位置,节点数变为父节点的数目。父节点变为左子节点的右孩子,子点数目为自己当前两个孩子之和+1。其余节点的子节点数目不变。可见这个过程不会增加复杂度的量级。
  通过这种方式的实现,可以知道插入一个节点的复杂度是O(lgn)O(lgn)O(lgn),找中位数的复杂度是O(lgn)。这个过程略微复杂,我就不贴代码,欢迎讨论。
  再继续分析,我们要求中位数,无非最多就是找两个数,如果我们把整个序列想象成排好序的序列,用这两个数把整个数据分段的话,这两个数分分别是前半段序列中最大的元素,后半段序列中的最小元素。也就是说,我们本可以不用去对序列排序,只需要方便找出最大和最小元素即可。

  树中还有一种特殊的结构叫堆,可以很快的查找出树中的最大/最小元素。但是一个堆找最大,一个堆找最小,我们就需要两个堆了,而且大顶堆的所有元素都小于小顶堆的所有元素。我们进一步分析发现,把中间部分较小的数放在第一个堆里,是第一个堆中最大的,我们使用大顶堆。中间部分较大的数放在第二个堆里,是第二个堆中最小的,我们使用小顶堆。这样就可以在O(1)O(1)O(1)的时间内求出中位数。
  这是序列长度为偶数的情况,如果长度为奇数,则只需要查找一个元素,我们可以让中间元素就是大顶堆的堆顶,这个时候,大顶堆包括了中间元素以及前半段,小顶堆只包含了后半段。大顶堆大序列数目比小顶堆多1。我们在插入的时候保证,总元素个数为奇数,则大顶堆比小顶堆元素多一个。
  我们进一步看看如何维护这两个堆,也就是插入的过程。总结来说,如果元素总个数为奇数,我们应该是大顶堆多一个元素。如果小顶堆的堆顶大于要插入的数,显然这个数出现在排序序列的前半段,可以直接将其插入大顶堆,然后调整堆即可。如果插入的数大于小顶堆堆顶,这个数据应该出现在序列的后半段,但是如果我们将其插入小顶堆,显然不符合我们的大顶堆的元素个数比小顶堆多的预设。我们这个时候,就得减少小顶堆的长度了。显然是小顶堆弹出堆顶,然后调整堆。这个堆顶的数现在应该去大顶堆了,然后插入大顶堆,调整堆。
  如果两个堆的总数目为奇数,根据我们的预设,大顶堆比小顶堆元素多1,现在要往小顶堆插入,分析就与上面的分析类似了。仍然是先判断插入元素是否小于大顶堆,判断插入的数应该出现在什么位置。如果这个数插入到大顶堆,大顶堆就得弹出堆顶,堆顶元素进入小顶堆。

C++代码如下

#include<vector>
#include<algorithm>
using namespace std;template<typename T>class DynamicArray
{public:void Insert(T num){if (min.size() ^ max.size()){if (num < max[0]){max.push_back(num);push_heap(max.begin(), max.end(), less<T>());num = max[0];pop_heap(max.begin(), max.end(), less<T>());max.pop_back();}min.push_back(num);push_heap(min.begin(), min.end(), greater<T>());}else{if (min.size() && num > min[0]){min.push_back(num);push_heap(min.begin(), min.end(), greater<T>());num = min[0];pop_heap(min.begin(), min.end(), greater<T>());min.pop_back();}max.push_back(num);push_heap(max.begin(), max.end(), less<T>());is_heap()}}T getMedian(){if (!max.size())throw exception("No elements are available");if (min.size() ^ max.size()){return max[0];}else{return (max[0] + min[0]) / 2;}}
private:vector<T>min;vector<T>max;
};

python代码

def less(left, right):return left<rightdef greater(left,right):return left>rightdef siftup(heap, startpos, pos,func=less):newitem = heap[pos]while pos>startpos:parentpos = (pos - 1) >> 1parent = heap[parentpos]if func(parent,newitem):heap[pos] = parentpos = parentposcontinuebreakheap[pos] = newitemdef siftdown(heap, pos,func=less,endpos=None):if endpos is None:endpos=len(heap)newitem = heap[pos]childpos = 2*pos + 1    # leftmost child positionwhile childpos<endpos:rightpos = childpos + 1if rightpos<endpos and func(heap[childpos],heap[rightpos]):childpos = rightposif func(newitem,heap[childpos]):heap[pos] = heap[childpos]pos = childposchildpos = 2*pos + 1else:breakheap[pos] = newitemdef heappush(heap, item,func=less):heap.append(item)siftup(heap, 0, len(heap) - 1,func)def heappop(heap,func=less):lastelt = heap.pop()if heap:returnitem = heap[0]heap[0] = lasteltsiftdown(heap, 0,func)return returnitemreturn lasteltclass DymaicArray():_min=[]_max=[]def getMedian(self):if not self._max:raise Exception('No elements are available')if len(self._max) ^ len(self._min):return self._max[0]else:return (self._max[0]+self._min[0])/2def insert(self,num):if len(self._max) ^ len(self._min):if num<self._max[0]:heappush(self._max,num,less)num=heappop(self._max,less)heappush(self._min,num,greater)else:if self._min and num>self._min[0]:heappush(self._min,num,greater)num=heappop(self._min,greater)heappush(self._max,num,less)

  python采用了自己实现大顶堆,小顶堆,所以代码就更加长一些。
  最后总述一下使用两个堆来操作的复杂度,求中位数就是取两个堆顶,时间复杂度是O(1)O(1)O(1),然后维护这样的堆,当有数据插入的时候时间复杂度是O(lgn)O(lgn)O(lgn),可见这种做法还是相当优秀的。下面表格将这几种算法的特性详细列出来。

基础数据结构 元素插入时间复杂度 取中位数的时间复杂度 特点描述
无序数组 O(1)O(1)O(1) O(n)O(n)O(n) 插入时间复杂度最小,适合频繁插入
有序线性表(数组,链表) O(n)O(n)O(n) O(1)O(1)O(1) 查找快,但是数据结构的维护困难
平衡二叉树 O(lgn)O(lgn)O(lgn) O(lgn)O(lgn)O(lgn) 查找和维护相对均衡,但是存储的额外信息多(指针,子节点数),查找其他元素也很快
最大堆和最小堆 O(lgn)O(lgn)O(lgn) O(1)O(1)O(1) 非常好的一种实现,但是查找其他元素效率低

从数据流中获取中位数相关推荐

  1. 《剑指offer》-- 序列化二叉树、二叉搜索树的第k个节点、数据流中的中位数、滑动窗口的最大值

    一.序列化二叉树: 1.题目: 请实现两个函数,分别用来序列化和反序列化二叉树. 2.解题思路: (1)根据前序遍历规则完成序列化与反序列化.所谓序列化指的是遍历二叉树为字符串:所谓反序列化指的是依据 ...

  2. 《剑指offer》数据流中的中位数

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

  3. 牛客网 在线编程 数据流中的中位数

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

  4. 【剑指offer】_18 数据流中的中位数

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

  5. 剑指offer:数据流中的中位数(小顶堆+大顶堆)

    1. 题目描述 /**如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值.如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的 ...

  6. 剑指Offer之寻找数据流中的中位数【包含大顶堆小顶堆解释】

    数据流中的中位数 题目描述 题解 最小堆和最大堆解释 参考链接 题目描述 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值.如果从数据流中读出偶 ...

  7. 剑指offer之数据流中的中位数

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

  8. 数据流中的中位数 c语言,41 数据流中的中位数(时间效率)

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

  9. 【剑指Offer】个人学习笔记_41_数据流中的中位数

    目录 题目: [剑指 Offer 41. 数据流中的中位数](https://leetcode-cn.com/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lc ...

最新文章

  1. 第十八讲 傅里叶变换
  2. 最新最全的用户画像系统详解,还可免费试用哦!
  3. 最近开机老是弹出网银插件的问题
  4. 怎么知道wx.config执行成功没_作为一个减肥40斤,且10年没反弹的普通人,这份瘦身经验分享给你...
  5. 【汇编优化】之X86汇编优化
  6. 区块链技术:颠覆性革命浪潮的开始
  7. 从0-1背包问题到动态规划
  8. Crackme008
  9. 【分享】学长的安利来了~~O(∩_∩)O
  10. MySQL--pt-osc工具学习
  11. 数学建模【开会总结】
  12. matlab 符号运算 简化,Matlab 符号运算的因式分解、展开与合并、简化
  13. 电影感悟-豆瓣TOP3
  14. 用Java创建一副扑克牌
  15. 利用kNN算法对iris数据集进行分类,本人也做了修改使得代码可实现
  16. 红孩儿编辑器的开发规范
  17. 大数据分析师·人才培养·高薪起航
  18. 多级延迟效果器:D16 Group Tekturon for Mac()
  19. windows函数(system)
  20. 点云配准--4PCS原理与应用

热门文章

  1. 本周最新文献速递20220313
  2. Ansible 之 Playbook详解
  3. MAC 查看java home目录
  4. 并发冲突控制与数据共享[原文发表时间:2005年3月19日]
  5. 蚂蚁区块链第13课 如何搭建一个DAPP应用(以姓名年龄为例)
  6. mPEG-FA 甲氧基PEG叶酸
  7. 互联网大厂持续裁员,疫情彻底放开,2023年你敢主动跳槽吗?
  8. K8S从私有仓库拉取镜像
  9. 过程挖掘(Process Mining)5——事件日志(Event Logs)(1):数据源与事件日志
  10. 写一个爬虫爬取boss直聘网站