目录
刷题日期:下午7:49 2021年5月7日星期五
个人刷题记录,代码收集,来源皆为leetcode
经过多方讨论和请教,现在打算往Java方向发力
主要答题语言为Java
题目:
剑指 Offer 41. 数据流中的中位数
难度困难141
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[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
次调用。
题目分析
基本没什么头绪,在返回中位数的过程中会遇到排序,然后判断。
初始解答:
困难题目,尝试构思一下思路
class MedianFinder {
/** initialize your data structure here. */
public MedianFinder() {
//这里需要考虑用什么数据结构来实现数据流
//需要同时满足排序和插入的快捷,参考书本考虑最大堆来实现
}
public void addNum(int num) {
//分为前后两个堆,由于本身有序,插入的位置也容易找到
}
public double findMedian() {
if (obj.length % 2 == 1) return obj[obj.length>>1]//为奇数的情况
if (obj.length % 2 == 0) return 0.5 * (obj[(obj.length>>1)-1]+obj[obj.length>>1])//为偶数的情况
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
看了书发现这题考的还有诸多数据存储类型的特点。看过评论区的解答长度就明白这题为啥是困难了。
学习方法三
class MedianFinder {
Queue<Integer> A, B; //两个堆,还是队列?和流比较接近
/** initialize your data structure here. */
public MedianFinder() {
//这里需要考虑用什么数据结构来实现数据流
//需要同时满足排序和插入的快捷,参考书本考虑最大堆来实现
A = new PriorityQueue<>(); //小顶堆,元素为较大那一部分
//这操作太骚了
B = new PriorityQueue<>((x, y) -> (y - x)); //大顶堆,元素为较小那一部分
}
public void addNum(int num) {
//分为前后两个堆,由于本身有序,插入的位置也容易找到
if(A.size() != B.size()) {
A.add(num); //如果前后不等,A先入队
B.add(A.poll()); //B接收A刚出队的元素,保持两堆一致
} else { //否则相反,如果两堆元素一样多,则加给后面
B.add(num);
A.add(B.poll());
}
}
public double findMedian() {
//如果两个一样多,则返回均值,否则返回小顶堆的堆顶
return A.size() != B.size() ? A.peek() : (A.peek() + B.peek()) / 2.0;
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
执行结果:通过
显示详情 添加备注
执行用时:85 ms, 在所有 Java 提交中击败了54.04%的用户
内存消耗:49.5 MB, 在所有 Java 提交中击败了76.21%的用户
学习他人:
方法一:
(编辑过)2020-03-11
用大顶堆+小顶堆方法,可以看作大顶堆是普通班,小顶堆是实验班。数量上时刻保持 小顶-大顶<=1(两堆相等或者小顶比大顶多一个)。
新学生先入普通班(大顶堆),此时可能会失去平衡了,于是取大顶堆的第一个(班里最好的学生)加入实验班(小顶堆),判断若数量过多(不是等于或多一个),取第一个(实验班里最差的学生)到普通班(大顶堆)里。 取中位数的时候,若两堆数量相等,则各取堆顶取平均,若小顶比大顶多一,则多的那一个就是中位数。
--------++++++++±-------
class MedianFinder {
PriorityQueue<Integer> left;//大顶
PriorityQueue<Integer> right;//小顶
public MedianFinder() {
left=new PriorityQueue<>((n1,n2)->n2-n1);
right=new PriorityQueue<>();
}
public void addNum(int num) {
left.add(num);
right.add(left.poll());
if(left.size()+1<right.size())
left.add(right.poll());
}
public double findMedian() {
if(right.size()>left.size())return right.peek();
return (double)(left.peek()+right.peek())/2;
}
}
方法二:
(编辑过)2020-03-14
维持一个大顶堆和小顶堆,确保:
1、大小顶堆元素数量差小于等于1
2、大顶堆中所有元素均小于小顶堆中元素
返回结果:
大小顶堆元素数量相等时,返回两个堆顶的平均值,否则返回较长堆的堆顶。
执行用时 :73 ms, 在所有 Java 提交中击败了94.29%的用户
内存消耗 :55.4 MB, 在所有 Java 提交中击败了100.00%的用户
class MedianFinder {
/** initialize your data structure here. */
List<Integer> maxHeap,minHeap;
public MedianFinder() {
maxHeap = new ArrayList<>();
minHeap = new ArrayList<>();
}
public void addNum(int num) {
if(maxHeap.size()==0){
maxHeap.add(num);
return;
}
if(num>maxHeap.get(0)){
minHeap.add(num);
DownToUp(minHeap,0);
}else{
maxHeap.add(num);
DownToUp(maxHeap,1);
}
//保持maxHeap和MinHeap长度之差小于等于1
while(Math.abs(maxHeap.size()-minHeap.size())>1){
if(maxHeap.size()>minHeap.size()){
minHeap.add(maxHeap.get(0));
DownToUp(minHeap,0);
maxHeap.set(0,maxHeap.get(maxHeap.size()-1));
maxHeap.remove(maxHeap.size()-1);
upToDown(maxHeap,1);
}else{
maxHeap.add(minHeap.get(0));
DownToUp(maxHeap,1);
minHeap.set(0,minHeap.get(minHeap.size()-1));
minHeap.remove(minHeap.size()-1);
upToDown(minHeap,0);
}
}
}
public double findMedian() {
if(maxHeap.size()==minHeap.size())
return (maxHeap.get(0)+minHeap.get(0))/2.0;
if(maxHeap.size()>minHeap.size())
return maxHeap.get(0);
return minHeap.get(0);
}
public void DownToUp(List<Integer> list,int flag){ //插入时向上调整
//flag = 0表示调整小顶堆,flag=1表示调整大顶堆
if(list.size()<=1)return;
int temp = list.get(list.size()-1);
int i =list.size()-1,j;
while(i!=0){
j=(i-1)/2;
if(flag == 0){//小顶堆
if(temp>=list.get(j))break;
list.set(i,list.get(j));
i=j;
}else{ //大顶堆
if(temp<=list.get(j))break;
list.set(i,list.get(j));
i=j;
}
}
list.set(i,temp);
}
public void upToDown(List<Integer> list,int flag){//删除时向下调整
//flag = 0表示调整小顶堆,flag=1表示调整大顶堆
if(list.size()<=1)return;
int temp = list.get(0);
int i =0,j,n=list.size()-1;
while(i!=list.size()-1){
j=i*2+1;
if(j>n)break;
if(flag == 0){//小顶堆
if(j<n&&list.get(j)>list.get(j+1))j++;
if(temp<=list.get(j))break;
list.set(i,list.get(j));
i=j;
}else{ //大顶堆
if(j<n&&list.get(j)<list.get(j+1))j++;
if(temp>=list.get(j))break;
list.set(i,list.get(j));
i=j;
}
}
list.set(i,temp);
}
}
方法三:
K神,堆实现复杂度分析
时间复杂度:
查找中位数 O(1)O(1) : 获取堆顶元素使用 O(1)O(1) 时间;
添加数字 O(\log N)O(logN) : 堆的插入和弹出操作使用 O(\log N)O(logN) 时间。
空间复杂度 O(N)O(N) : 其中 NN 为数据流中的元素数量,小顶堆 AA 和大顶堆 BB 最多同时保存 NN 个元素。
作者:jyd
链接:https://leetcode-cn.com/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof/solution/mian-shi-ti-41-shu-ju-liu-zhong-de-zhong-wei-shu-y/
来源:力扣(LeetCode)
Java 使用 PriorityQueue<>((x, y) -> (y - x))
可方便实现大顶堆。
class MedianFinder {
Queue<Integer> A, B;
public MedianFinder() {
A = new PriorityQueue<>(); // 小顶堆,保存较大的一半
B = new PriorityQueue<>((x, y) -> (y - x)); // 大顶堆,保存较小的一半
}
public void addNum(int num) {
if(A.size() != B.size()) {
A.add(num);
B.add(A.poll());
} else {
B.add(num);
A.add(B.poll());
}
}
public double findMedian() {
return A.size() != B.size() ? A.peek() : (A.peek() + B.peek()) / 2.0;
}
}
总结
实现题目要求不同数据存储结构的时间复杂度:
数据结构 | 插入的时间复杂度 | 得到中位数的时间复杂度 |
---|---|---|
未排序数组 | O(1) | O(n) |
排序数组 | O(n) | O(1) |
排序链表 | O(n) | O(1) |
二叉搜索树 | 平均O(logn)最差O(n) | 平均O(logn)最差O(n) |
AVL树 | O(logn) | O(1) |
最大堆和最小堆 | O(logn) | O(1) |
AVL树的时间效率很高,但大部分编程语言的函数库中都没有实现这个数据结构,所以考虑堆实现。
以上就是本题的内容和学习过程了,分析得出堆的过程本身就很不容易了,考验基本功,不愧是困难。
欢迎讨论,共同进步。