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

本文介绍了如何使用最大堆和最小堆解决数据流中中位数的计算问题,详细分析了多种方法,包括使用未排序数组、排序数组、排序链表、二叉搜索树和堆等数据结构的优缺点。重点讲解了使用两个堆实现的解决方案,通过维护大小顶堆的平衡,确保在O(logN)的时间复杂度内完成插入和获取中位数的操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

刷题日期:下午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;
    }
}

方法二:

ZML400

(编辑过)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树的时间效率很高,但大部分编程语言的函数库中都没有实现这个数据结构,所以考虑堆实现。

以上就是本题的内容和学习过程了,分析得出堆的过程本身就很不容易了,考验基本功,不愧是困难。

欢迎讨论,共同进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值