前言

大家好,我是bigsai。最近轻敌了一个高频问题,分享给大家。

最近面试时候遇到一个非常有意思的hard题,面试官没让写代码让说思路,但放在正常应届生招聘那可能就要手撕了,在剑指offer的第41题和力扣【数据流中的中位数】。

题目描述是这样的:

中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

例如,

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

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

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

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

其实问题也很简单,也就是一组数据,找出它的中位数,然后有所不同的是这组数据可能会新增一些其他数据,也就是要我们自己维护这么一个数据结构去尽量高效的完成它。

我打开这题力扣上的额描述,它好像就在诱惑我,告诉我什么!

然后我就以为真实的数据就在这个范围,然后一顿操作猛如虎,一提交直接GG。不过这个问题是个非常好的问题,等到后面讲,仔细先看看!

好了不多说了,我们进入正题,看看这个经典问题到底如何分析。

小白级别方法(无脑排序)

这个问题的解决老少皆宜,小白也有小白的方法,题不在难,能过就行(只对小白有效哇)。

一组数据存储,我用数组、List都可以,而中位数,其实就是中间一个(偶数两个均值)数,这个也好办啊,排序啊!

一个ArrayList()海纳百川(不定长数组可以存储数据),一行Arrays.sort()走天下。用编程语言中已经存在的集合和API可以轻松实现!

分析一下这个算法的时间复杂度,每次插入时候需要排序 nlogn,查询时间近O(1);次数多的话时间复杂度还是蛮高的。

具体代码为:

/** initialize your data structure here. */
public MedianFinder() {}List<Integer>list=new ArrayList<>();
public void addNum(int num) {list.add(num);list.sort(null);
}
public double findMedian() {if(list.size()%2==1)return list.get(list.size()/2);else {return (double) (list.get((list.size()-1)/2)+list.get(list.size()/2))/2;}
}

一看结果,2000+ms,这也是一种极限了,这种方法仅限于小白,只为求过。

大白级别的方法(插入排序)

大白比小白稍微强一点,比较它大一点嘛,懂得多一点。

大白看到:**这个序列刚开始没有!**然后一点一点在原有的基础上增加长度,如果每次都打乱排序那代价有点高!所以能不能复用上次已经排好序的结果?

这不就插入排序嘛!

每次新来元素相当于插入排序的最后一步使他有序,然后在插入……

这个流程每次插入的时候由两个部分组成,查找和移动,其中查找花费O(n),插入也是O(n)。时间复杂度依然是O(n)。

可以维护常数表示数据总个数,查找中位数时候可以直接根据数量查找,时间复杂度为O(1).这样的时间复杂度在插入上优化为O(n)相比O(nlogn)有很大的提升。

但是聪明的大白还能发现一些闪光点,数组前面有序的,只是插入最后一个元素需要锁定在有序顺序结构中的位置,线性一个个查找太耗时了哇!这明显就是一个二分应用的场景么!可以使用二分法找到插入的位置,然后插入。

二分+插入的时间复杂度是多少呢?

记住,不是O(logn),二分查找每一次的时间复杂度确实是O(logn),但插入操作的时间复杂度依旧是O(n),总的时间复杂度取最高量级 O(logn)+O(n)=O(n)。

所以这里以后如果遇到某个面试官问你:插入排序使用二分查找优化时间复杂度是多少?你一定要坚定的回答:是O(n2),从单词操作来看,虽然查询变为O(logn)但是插入依旧O(n),所以n个数时间复杂度依旧为O(n2)。

好了既然解释清楚,一顿操作猛如虎,直接上代码:

class MedianFinder {/** initialize your data structure here. */public MedianFinder() {}int arr[]=new int[50000];int count=0;public void addNum(int num) {int low=0,high=count-1;while (low <= high) {int mid = (low + high) / 2;int midVal = arr[mid];if (midVal < num)low = mid + 1;else if (midVal > num)high = mid-1 ;else low++;}for(int i=count-1;i>=low;i--){arr[i+1]=arr[i];}arr[low]=num;count++;}public double findMedian() {if(count%2==1)return arr[count/2];else return (double) (arr[count/2-1]+arr[count/2])/2;}
}

提交之后,200+ms,比起前面小白的无脑排序优化了很多,但只击败了10%用户,说明方法还是不够好,还是很白

大佬的解法(双堆/优先队列)

这个问题的话其实也能想出来,但是确实除了这个问题没见到两个队这么玩的,也算是打开眼界一回。

在前面,我们讲过堆排序 和优先队列的原理 ,里面详细的讲解了 堆这种数据结构,我们简单回顾一下堆:

堆是一种完全二叉树(即最后一层从左向右存在节点连续),因为是完全二叉树我们经常用数组存储,可以通过二叉树下标转换很好的锁定位置进行交换。

堆分为大根堆小根堆

如果所有节点大于孩子节点值,那么这个堆叫做大根堆,堆的最大值在根节点。

如果所有节点小于孩子节点值,那么这个堆叫做小根堆,堆的最小值在根节点。

了解完这些基本解决这题就够了,这里不需要知道堆、优先队列的具体实现。但是堆和优先队列,怎么应用到这个中位数查找呢?

这个就很巧妙了,我们将数据等半到两个堆中,其中一个是小根堆,一个是大根堆,小根堆存最大的一半数据,大的中最小的在堆顶;大根堆存最小的一半数据,小的中最大的在堆顶,中位数就只可能在两个堆顶部分产生啦!

但是在具体实现过程中,也有很多需要注意的地方,再添加时候要先判断和其中一个堆顶比较大小,应该加到哪一个堆(优先队列)中,但是添加之后可能数值一直递增很大或者数值一直递减很小可能造成两个堆元素数量不平衡,那么就要将其中少的加到多的中。

这里我在实现的时候约束小根堆的元素个数等于大根堆个数(偶数)或者等于大根堆个数加一(奇数),在奇数情况就直接取小根堆顶返回即可。因为Java已经实现优先队列,你不需要详细实现其中细节(大佬可以试试参考以前我写的优先队列实现)。

分析这个时间复杂度,每个堆插入、删除的时间复杂度级别是O(log n/2),即使如果面临元素平衡可能多操作两次,但是时间复杂度还是O(logn)级别。比起O(n)快了不少,数据量越大体现越明显。

具体代码为:

class MedianFinder {/** initialize your data structure here. */public MedianFinder() {}Queue<Integer>q1=new PriorityQueue<>();//小根堆 放大的数据//大根堆Queue<Integer>q2=new PriorityQueue<>(new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {return o2-o1;}});public void addNum(int num) {if(q1.isEmpty())q1.add(num);else if(num>q1.peek()){//插入到小根堆q1.add(num);if (q1.size()>q2.size()+1){q2.add(q1.poll());}}else{//插入到大根堆q2.add(num);if (q2.size()>q1.size()){q1.add(q2.poll());}}}public double findMedian() {if(q1.size()==q2.size())return (double)(q1.peek()+q2.peek())/2;else return q1.peek();}
}

执行区间75ms,比起上一个大白又好很多,这个双堆,真的太妙了!我也是第一次见。

提升

对于这个问题,还有一些妖魔鬼怪用二叉搜索树来做,理论上也是可行的,插入效率不一定很稳定,查询效率比较低(二叉树的中序排序)。

但是文初提到的场景问题还是要仔细思考一下,面试场景很可能会问到:

1.如果数据流中所有整数都在 0 到 100 范围内,你将如何优化你的算法?

2.如果数据流中 99% 的整数都在 0 到 100 范围内,你将如何优化你的算法?

对于第一个问题,应该用什么方法优化呢?

如果还是采用传统的双堆,如果数据量非常大的情况下,效率肯定还是有优化空间,当数据比较集中分布的时候,肯定要考虑计数排序啊!

如果数据分布在0-100之间,可以直接开一个101大小的数组,遇到一个数就将其对应位置值加一,不光0-100 可以这样,在某个范围内都可以通过移动表示。

然后你找到中位数,只需要枚举叠加找到中间位置的数啦。

不过你可以再进行优化,将这个计数排序修改一下,用数组值表示小于当前位置值的个数。这样这个数组值是连续递增的,查找的时候还可以使用二分优化

不过具体实现上,可能还有一些你需要考虑的,分析算法复杂度,每次插入操作,对应小标以及之后值都要加一,所以操作次数不超过50,而查询操作使用二分法也不超过10次,所以在数据比较集中 数据个数很多有大量重复情况,使用计数排序是非常好的。

另外,如果99%在0-100范围内也很容易哇,就是在前后边缘特意设置0和102,超过100的放到102号,小于0的放到0位置,剩下每次来x放x+1位置,因为中位数肯定出现在0-100所以这个依旧可行!

本文收藏再开源数据结构与算法仓库中:

https://github.com/javasmall/bigsai-algorithm

关于作者

bigsai,在读研一,手握腾讯、字节实习offer,蓝桥杯国一选手,专注于数据结构与算法、Java领域的知识分享,喜欢用图将复杂内容简单化,欢迎关注同名公众号【bigsai】,坚持输出干货,如果有学习、实习、考研、选择等问题欢迎交流!

数据流中的中位数,我轻敌了相关推荐

  1. 剑指offer:面试题41. 数据流中的中位数

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

  2. Leetcode295 数据流中的中位数-最小堆和最大堆

    题目 中位数是有序列表中间的数.如果列表长度是偶数,中位数则是中间两个数的平均值. 例如,[2,3,4] 的中位数是 3:[2,3] 的中位数是 (2 + 3) / 2 = 2.5 设计一个支持以下两 ...

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

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

  4. 数据结构与算法之字符凭拼接最低字典序和数据流中取中位数

    数据结构与算法之字符凭拼接最低字典序和数据流中取中位数 目录 字符凭拼接最低字典序 数据流中取中位数 1. 字符凭拼接最低字典序 题目描述 思路: 创建一个比较器,比较的内容是(o1+o2).comp ...

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

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

  6. [剑指Offer] 数据流中的中位数

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

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

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

  8. Python中的堆实现:heapq 模块——利用堆结构实现快速访问数据流中的中位数

    堆结构 堆结构是一种优先队列,可以以任意顺序添加对象,并随时查找或删除最小(大)的元素,或者查找和删除前 K 个最小(大)元素.相比于列表方法min() / max(),这样做的效率要高得多. 堆结构 ...

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

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

最新文章

  1. fmpeg 编译编码 _libx264和librtmp
  2. Python re(正则表达式)简明教程
  3. 把txt中的数据读出并保存到数组中
  4. HDU - 5459 Jesus Is Here(思维+非线性递推)
  5. 用32定时器让电机缓慢转动_实验三 步进电机原理及应用
  6. 【CSS】小妙招,各种问题总结方法处理
  7. leetcode 700. 二叉搜索树中的搜索 思考分析
  8. php按文章评论数排序,zblog获取分类文章排序按指定的时间排序、评论数量排序、浏览数量排序...
  9. [Java] 蓝桥杯ALGO-125 算法训练 王、后传说
  10. 如何在Mac OS X 10.6.4上卸载Python 2.7?
  11. NJ法,ML法构建系统发育树~MEGA7.0,iqtree(图文教程)
  12. 毕业设计c语言课程设计,大学毕业设计论文-—c语言课程设计.doc
  13. 30天打造专业红客四
  14. 行路难 李白
  15. 洛谷3386二分图模板
  16. kubebuilder 上手体验
  17. java.lang.classcastexception怎么解决_抛出奇怪的java.lang.ClassCastException
  18. HTML中alt标签的用法
  19. H5自适应屏幕分辨率大小
  20. 学生信息结构体数组拷贝和排序(结构体,结构体数组,结构体指针)

热门文章

  1. EOS账户系统(6)权限和Action映射
  2. 区块链BaaS云服务(29) 溪塔科技 CITA-Cloud 二
  3. (chap6 Http首部) 通用首部字段 DatePragma
  4. crypto——明文攻击
  5. 【Flask】自定义转换器
  6. Chrome好用的插件
  7. android 判断时间是否过期_眉笔眉粉有保质期吗?怎么判断眉笔眉粉是否过期?...
  8. 【攻防世界005】parallel-comparator-200
  9. 宏病毒的研究与实例分析06——终结篇 进击的MACRO
  10. python2 去除 字符串中emoji 符号,去除所有4字节utf8字符