求中位数?

news2024/9/27 5:45:59

文章目录

  • 如何求一个中位数?
    • [295. 数据流的中位数](https://leetcode.cn/problems/find-median-from-data-stream/)
      • 常规思路:
      • 解决思路:

如何求一个中位数?

如果输入一个数组,让你求中位数,简单的解决方法就是对数组进行排序

如果数组长度是奇数,最中间的一个元素就是中位数

如果数组长度是偶数,最中间两个元素的平均数作为中位数。

但是如果数据规模非常巨大,排序不太现实,那么也可以使用概率算法,随机抽取一部分数据,排序,求中位数,作为所有数据的中位数。

295. 数据流的中位数

这道题就是让我们设计这样一个类:

class MedianFinder {

    // 添加一个数字
    public void addNum(int num) {}

    // 计算当前添加的所有数字的中位数
    public double findMedian() {}
}

常规思路:

常规的思路就是

用一个数组记录所有addNum添加进来的数字,通过插入排序的逻辑保证数组中的元素有序,当调用findMedian方法时,可以通过数组索引直接计算中位数

但是用数组作为底层容器的问题也很明显:

addNum搜索插入位置的时候可以用二分搜索算法,但是插入操作需要搬移数据,最坏时间复杂度为O(N)

因为数组的插入时间复杂度问题,考虑使用链表

使用链表插入数据很快,但是查找插入位置的时候只能线性遍历,最坏时间复杂度为 O(N)

而且findMedian方法也需要遍历寻找中间索引,最坏时间复杂度也是 O(N)

因为链表的查找时间复杂度问题,考虑使用平衡二叉树

平衡二叉树的增删查改的复杂度都为 O(logN)

比如使用Java提供的TreeSet容器,底层是红黑树,addNum直接插入,findMedian可以通过当前元素的个数推算出计算中位数元素的排名

但是,TreeSet是一种Set其中不存在重复元素的元素,但是我们的数据流可能输入重复数据,而且计算中位数也是需要算上重复元素的

不仅如此,TreeSet并没有实现一个通过排名快速计算元素的API。也就是说加入我们想找到TreeSet中的第5大的元素我们需要手动去实现这个需求

平衡二叉树也不行,那**优先级队列(二叉堆)**可以吗?

优先级队列是一种受限的数据结构,只能从堆顶添加/删除元素,我们的addNum方法可以从堆顶插入元素,但是findMedian函数需要从数据中间取,这个功能优先级队列是没办法提供的

解决思路:

解决这个问题我们必然是会用到有序数据结构的,本题所用到的数据结构是两个优先级队列

中位数是有序数组最中间的元素

我们可以将有序数组抽象成一个倒三角形(从大到小),宽度可以视为元素的大小,那么这个倒三角形的中部就是计算中位数的元素

将这个倒三角形从中间切成两半,编程一个小倒三角形和一个梯形

这个小的倒三角形相当于一个从小到大的有序数组,这个梯形相当于一个从大到小的有序数组

他们分别可以是大顶堆和小顶堆,中位数就是他们的堆顶元素

但是梯形虽然是小顶堆,但其中的元素是较大的,我们称其为large,倒三角虽然是大顶堆,但是其中元素较小,我们称其为small

当然,这两个堆需要算法逻辑正确维护,才能保证堆顶元素是可以算出正确的中位数,我们很容易看出来,两个堆中的元素之差不能超过 1。(这其实就是在限制实现addNum方法)

假设元素总数是n

  • 如果n是偶数,我们希望两个堆的元素个数是一样的,这样把两个堆的堆顶元素拿出来求个平均数就是中位数;
  • 如果n是奇数,那么我们希望两个堆的元素个数分别是n/2 + 1n/2,这样元素多的那个堆的堆顶元素就是中位数。

因此,我们可以得到代码如下:

class MedianFinder {

    private PriorityQueue<Integer> large;
    private PriorityQueue<Integer> small;

    public MedianFinder() {
        // 小顶堆
        large = new PriorityQueue<>();
        // 大顶堆
        small = new PriorityQueue<>((a, b) -> {
            return b - a;
        });
    }

    public double findMedian() {
        // 如果元素不一样多,多的那个堆的堆顶元素就是中位数
        if (large.size() < small.size()) {
            return small.peek();
        } else if (large.size() > small.size()) {
            return large.peek();
        }
        // 如果元素一样多,两个堆堆顶元素的平均数是中位数
        return (large.peek() + small.peek()) / 2.0;
    }

    public void addNum(int num) {
        // 后文实现
    }
}

那addNum方法具体要如何实现呢?

每次调用addNum方法的时候都比较以下large和small的元素个数?谁的元素少就加到谁那里,如果它们的元素一样多,默认加到large里面

// 有缺陷的代码实现
public void addNum(int num) {
    if (small.size() >= large.size()) {
        large.offer(num);
    } else {
        small.offer(num);
    }
}

但是这样还是会有问题的,比如

addNum(1),现在两个堆元素数量相同,都是0,所以默认把1添加到large堆。

addNum(2),现在large的元素比small的元素多,所以把2添加到small堆中。

addNum(3),现在两个堆都有一个元素,所以默认把3添加到large中。

调用findMedian,预期的结果应该是2,但是实际的到的结果是1

在这里插入图片描述
由图可以得到一个事实就是我们的梯形和小倒三角形都是由原始的大倒三角从中间切开得到的,那么梯形中的最小宽度要大于等于小倒三角的最大宽度,这样它俩才能拼成一个大的倒三角!

也就是说,我们在addNum的时候不仅要维护large和small的元素个数之差不超过1,还要维护large堆的堆顶元素要大于等于small堆的堆顶元素

那我们如何实现呢?

我们可以这样实现,当我们想要往large里添加元素,不能直接添加,而是要先往small里添加,然后再把small的堆顶元素加到large中,向small中添加元素同理

这样做的原理是什么呢?

假设我们准备向large中插入元素:

如果插入的num小于small的堆顶元素那么num就会留在small堆里,

为了保证两个堆的元素数量之差不大于1,作为交换,把small堆顶部的元素再插到large堆里

如果插入的num大于small的堆顶元素,那么num就会成为small的堆顶元素,最后还是会被插入large堆中

反之,向small中插入元素是一个道理,这样就巧妙地保证了large堆整体大于small堆且两个堆的元素之差不超过1,那么中位数就可以通过两个堆的堆顶元素快速计算了

// 正确的代码实现
public void addNum(int num) {
    if (small.size() >= large.size()) {
        small.offer(num);
        large.offer(small.poll());
    } else {
        large.offer(num);
        small.offer(large.poll());
    }
}

addNum方法时间复杂度 O(logN),findMedian方法时间复杂度 O(1)。

完整代码如下:

class MedianFinder {
    private PriorityQueue<Integer> small;
    private PriorityQueue<Integer> large;
    public MedianFinder() {
        small=new PriorityQueue<>();
        large=new PriorityQueue<>((a,b)->{
            return b-a;
        });
    }
    
    public void addNum(int num) {
        //当两个堆的元素个数相同的时候,向large中添加元素,在向large添加元素之前,先将元素添加到small,再将small的堆顶元素添加到large
        if(small.size()>=large.size()){
            small.offer(num);
            large.offer(small.poll());
        }else{
            large.offer(num);
            small.offer(large.poll());
        }
    }
    
    public double findMedian() {
        //如果两个堆的元素相同就返回两个堆的堆顶元素平均值
        //如果两个堆的元素个数不相同就返回元素个数比较多的堆顶元素
        if(small.size()>large.size()){
            return small.peek();
        }else if(small.size()<large.size()){
            return large.peek();
        }
        return (small.peek()+large.peek())/2.0;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/495113.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

不合格机器人工程讲师如何坦然面对失败

能坦然面对的重要原因是我自己从一开始就知道注定要失败的&#xff0c;或者说失败率接近99.9%。 能够从失败中成长&#xff0c;不断挑战自我&#xff0c;收获更多更大的失败也是一种体验。 失败是客观事实&#xff0c;坦然是主观上的正能量的情绪。 用客观态度对待客观事实。 …

JavaScript class和继承的原理

&#xff08;对于不屈不挠的人来说&#xff0c;没有失败这回事。——俾斯麦&#xff09; class 相关链接 MDN链接 有关类的详细描述 关于构造函数&#xff0c;原型和原型链的说明 类的概述 类是用于创建对象的模板。他们用代码封装数据以处理该数据。JS 中的类建立在原型上…

4月跳槽进字节跳动了,面试真简单...

前言: 最近金三银四跳槽季&#xff0c;相信很多小伙伴都在面试找工作&#xff0c; 怎样才能拿到大厂的offer&#xff0c;没有掌握绝对的技术&#xff0c;那么就要不断的学习&#xff0c;没有绝对的天才&#xff0c;只有持续不断的付出。对于我们每一个平凡人来说&#xff0c;…

Detecting Kernel Memory Leaks in Specialized Modules with Ownership Reasoning

Detecting Kernel Memory Leaks in Specialized Modules with Ownership Reasoning 背景&#xff1a; 内存泄漏&#xff1a;A memory leak happens when an allocated memory region is not released even though it will never be used again.分配的内存未被正常释放。 内存…

Java高阶数据结构 图 图的表示与遍历

高阶数据结构&#xff01; 文章目录 Java高阶数据结构 & 图的概念 & 图的存储与遍历1. 图的基本概念1.1 图的属性1.2 无向图与有向图1.3 完全图1.4 简单路径和回路1.5 子图1.6 连通图 2. 图的存储&#xff08;理论&#xff09;2.1 ※邻接矩阵2.2 邻接链表3. 图的存储&a…

新唐NUC980使用记录(5.10.y内核):u-boot linux rootfs 编译与烧录测试(基于SD1位置SD卡)

文章目录 目的基础准备烧录环境开发编译环境SD卡分区 制作和设置编译工具链制作toolchain和rootfs拷贝rootfs内容到SD卡设置编译工具链 u-boot编译与测试下载、配置与编译烧录u-boot与环境变量 linux kernel编译与测试下载、配置与编译系统运行测试 总结设备树文件内容 目的 从…

【STM32】基础知识 第十一课 sys, delay usart 文件夹

【STM32】基础知识 第十一课 sys, delay & usart 文件夹 sys 文件介绍delay 文件夹函数简介SysTickSysTick 工作原理SysTick 寄存器介绍 delay_init() 函数delay_us() 函数usart 文件夹介绍printf 的使用常用输出控制符表常用转椅字符表 半主机模式简介 sys 文件介绍 函数…

浅谈Linux epoll机制

前言 概述 epoll是一种当文件描述符的内核缓冲区非空的时候&#xff0c;发出可读信号进行通知&#xff0c;当写缓冲区不满的时候&#xff0c;发出可写信号通知的机制&#xff1b; 关键函数 int epoll_create(int size);创建 eventpoll 对象&#xff0c;并将 eventpoll 对象…

Python+Yolov5墙体桥梁裂缝识别

程序示例精选 PythonYolov5墙体桥梁裂缝识别 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<PythonYolov5墙体桥梁裂缝识别>>编写代码&#xff0c;代码整洁&#xff0c;规则&am…

【安装Nginx】

Linux上安装Nginx 文章目录 Linux上安装NginxUbuntuCentOS查看已安装的软件 Ubuntu 在 Ubuntu 上安装 Nginx 非常简单。只需按照以下步骤操作&#xff1a; 打开终端&#xff0c;更新软件包索引&#xff1a; sudo apt update安装 Nginx&#xff1a; sudo apt install nginx安…

2023世界超高清视频产业发展大会博冠8K明星展品介绍

2023世界超高清视频产业发展大会博冠8K明星展品介绍&#xff1a; 一、博冠8K全画幅摄像机B1 这是一款面向广电应用的机型&#xff0c;可适配外场ENG制作轻量化需求&#xff0c;应用于8K单边机位、新闻、专题的拍摄工作&#xff0c;也可应用于体育转播、文艺节目等特殊机位及各…

使用react脚手架初始化项目

1.使用react脚手架初始化项目 1.初始化命令&#xff0c;&#xff08;create-react-app脚手架的名称&#xff0c;my-app项目名称&#xff09; npx create-react-app my-app2.初始化完成之后可以看到Happy hacking&#xff01; 3.启动项目&#xff0c;进去根目录cd my-app &am…

这才是 玩转Github 的正确姿势

这才是 玩转Github 的正确姿势 GitHub各位应该都很熟悉了&#xff0c;全球最大的开源社区&#xff0c;也是全球最大的同性交友网站~~&#xff0c;但是大部分同学使用GitHub应该就是通过别人的开源链接&#xff0c;点进去下载对应的项目&#xff0c;而真正使用Github来查找开源…

RF学习-RF基本概念

射频(RF)指的是无线波的频率在3KHZ~300GHZ&#xff1b; RF系统通常包括以下模块&#xff1a; 本振(LO)放大器(Amplifier)混频器(Mixer)滤波器(Filter)天线(Antenna) RF接收机有如下三种类型&#xff1a; 超外差式接收机(Super heterodyne structure)零中频接收机(Homodyne…

GAN与DCGAN

GAN&#xff1a;生成对抗网络&#xff0c;首先是一个生成模型&#xff0c;区别与之前的辨别模型&#xff0c;对抗体现在生成器与辨别器之间的对抗。 生成器输入的是噪音&#xff0c;通过多层的MLP可以产生图片&#xff0c;将产生的图片和真实图片输入到辨别器&#xff0c;辨别器…

AI绘画5大免费工具

AI现在最火爆的两个方向一个是以ChatGPT为主导的文本生成工具&#xff0c;还有一个就是以Midjourne为主导的文本生成图片工具。 Midjourne 现在基本是都是需要收费的&#xff0c;但确实Midjourne的效果是顶尖的&#xff0c;如果我们只是想试一下 文本生图的过程&#xff0c;这里…

【ArcGIS Pro二次开发】(26):数据筛选器

在使用【OpenItemDialog】打开数据时&#xff0c;其中一个重要的属性【Filter】&#xff0c;可用于筛选要打开的数据。示例代码如下&#xff1a; // 打开文件对话框OpenItemDialog dlg new OpenItemDialog(){Title "选择要打开的文本文件",Filter ItemFilters.Dat…

如何用ChatGPT写专业方向的科普内容?

该场景对应的关键词库&#xff08;13个&#xff09;&#xff1a; 目标用户、科普内容、生活问题、医疗类型、科普文章、病情症状、通俗性、专业名词、背景资质、权威领域、执业范围、证言人、内容形式。 提问模板&#xff08;3个&#xff09;&#xff1a; 第一步&#xff0c;…

打包工具--pyinstaller

下载库 pip install pyinstaller 打包命令 Pyinstaller -D setup.py 打包exePyinstaller -F -w run.py 不带控制台的打包Pyinstaller -F -i xx.ico setup.py 打包指定exe图标打包 ❝ -D&#xff1a;打包为一个文件夹&#xff0c;其中exe文件在文件夹内部&#xff0c;这样子单个…

更换外线和智能电表后家里用电频繁跳闸的检修

老家的电路老是跳闸。今天检修了老家的线路&#xff0c;故障就是更换了外线路后&#xff0c;家里烧水或者用电磁炉就频繁跳闸。其实也说不清楚&#xff0c;因为最近又改了智能表嘛。 到电表处观察&#xff0c;是插卡智能表&#xff0c;电表进线有个空开C63A。电表出来有个空开C…