当前位置: 首页 > news >正文

【LeetCode 热题 100】295. 数据流的中位数——最大堆和最小堆

Problem: 295. 数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如 arr = [2,3,4] 的中位数是 3 。
例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
实现 MedianFinder 类:
MedianFinder() 初始化 MedianFinder 对象。
void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:
    • 空间复杂度:O(N)

整体思路

这段代码旨在解决一个经典的数据结构设计问题:数据流中的中位数 (Find Median from Data Stream)。问题要求设计一个数据结构,它能支持两个操作:addNum(从数据流中添加一个整数)和 findMedian(返回当前所有数字的中位数)。

该实现采用了一种非常经典且高效的 双堆(Dual Heaps) 方法。它巧妙地利用两个优先队列(堆)来动态维护数据流的中位数。

  1. 核心思想:分割数据流

    • 算法将所有已添加的数字逻辑上分为两部分:
      • 较小的一半:存储在一个 最大堆 (Max Heap) left 中。
      • 较大的一半:存储在一个 最小堆 (Min Heap) right 中。
    • 通过这种方式,left 堆的堆顶元素是“较小一半”中的最大值,而 right 堆的堆顶元素是“较大一半”中的最小值。这两个堆顶元素就构成了整个数据流的“中心”。
  2. 维护两个不变量

    • 不变量1(数值关系)left 堆中的所有元素都小于或等于 right 堆中的所有元素。
    • 不变量2(数量关系)left 堆的大小总是等于或比 right 堆大 1。即 left.size() == right.size()left.size() == right.size() + 1
  3. addNum(int num) 方法的实现

    • addNum 方法的核心任务是在添加新元素 num 后,依然维持上述两个不变量。
    • 当两堆大小相等时 (left.size() == right.size())
      • 我们期望最终 left 堆比 right 堆多一个元素。
      • 为了维护数值关系,不能直接将 num 加入 left。一个巧妙的操作是:先将 num 加入 right 堆,然后从 right 堆中弹出最小值(即 right.poll()),再将这个最小值加入 left 堆。
      • 这个“中转”操作确保了新加入 left 堆的元素一定是“较大一半”中的最小值,从而维持了 left 所有元素 <= right 所有元素的不变量。
    • leftright 多一个元素时 (left.size() > right.size())
      • 我们期望最终两堆大小相等。
      • 类似地,先将 num 加入 left 堆,然后从 left 堆中弹出最大值(left.poll()),再将这个最大值加入 right 堆。
      • 这个操作确保了新加入 right 堆的元素一定是“较小一半”中的最大值,维持了不变量。
  4. findMedian() 方法的实现

    • findMedian 的实现非常简单,直接利用了双堆的结构和不变量。
    • 如果数据总数为奇数left 堆会比 right 堆多一个元素。此时,中位数就是 left 堆的堆顶元素 left.peek()
    • 如果数据总数为偶数left 堆和 right 堆大小相等。此时,中位数是 left 堆的堆顶(“较小一半”的最大值)和 right 堆的堆顶(“较大一半”的最小值)的平均值。

完整代码

class MedianFinder {// left: 一个最大堆,用于存储数据流中较小的一半元素。// Java的PriorityQueue默认是最小堆,通过自定义比较器 (a, b) -> b - a 实现最大堆。private final PriorityQueue<Integer> left = new PriorityQueue<>((a, b) -> b - a);// right: 一个最小堆,用于存储数据流中较大的一半元素。// 默认构造函数创建的就是最小堆。private final PriorityQueue<Integer> right = new PriorityQueue<>();/** 构造函数,无需特殊操作。*/public MedianFinder() {}/*** 向数据结构中添加一个整数。* @param num 要添加的数字*/public void addNum(int num) {// 目标:维持 left.size() == right.size() 或 left.size() == right.size() + 1// 当前总数为偶数,添加后将变为奇数。目标是让 left 比 right 多一个。if (left.size() == right.size()) {// 为了维持 left 中所有元素 <= right 中所有元素的不变量:// 1. 先将 num 加入 right 堆。right.offer(num);// 2. 从 right 堆中弹出最小值,并将其加入 left 堆。//    这样保证了新加入 left 的元素是正确的。left.offer(right.poll());} else { // 当前总数为奇数,添加后将变为偶数。目标是让两堆大小相等。// 类似地,为了维持不变量:// 1. 先将 num 加入 left 堆。left.offer(num);// 2. 从 left 堆中弹出最大值,并将其加入 right 堆。right.offer(left.poll());}}/*** 返回当前数据流的中位数。* @return 中位数*/public double findMedian() {// 如果总数为奇数,left 堆会多一个元素,中位数就是 left 堆顶。if (left.size() > right.size()) {return left.peek();}// 如果总数为偶数,两堆大小相等,中位数是两个堆顶的平均值。// 注意要除以 2.0 来确保结果是浮点数。return (left.peek() + right.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();*/

时空复杂度

假设已经向数据结构中添加了 N 个数字。

时间复杂度:

  1. addNum(int num): O(log N)

    • 该方法主要包含堆的插入 (offer) 和删除 (poll) 操作。
    • PriorityQueue 在Java中是基于二叉堆实现的。对于一个大小为 k 的堆,插入和删除操作的时间复杂度都是 O(log k)
    • 在我们的实现中,left 堆和 right 堆的大小都约等于 N/2
    • 因此,addNum 方法执行的操作(如 right.offer, right.poll, left.offer)的时间复杂度都是 O(log(N/2)),这等价于 O(log N)
  2. findMedian(): O(1)

    • 该方法只需要访问两个堆的堆顶元素 (peek)。
    • 访问堆顶元素是一个常数时间操作。
    • 因此,findMedian 的时间复杂度是 O(1)

空间复杂度:O(N)

  1. 主要存储开销:算法的主要空间开销来自于两个优先队列 leftright
  2. 空间大小:这两个堆共同存储了所有已添加的 N 个数字。
  3. 综合分析
    • left 堆存储约 N/2 个元素,right 堆存储约 N/2 个元素。
    • 因此,总的空间复杂度与已添加的数字数量 N 成线性关系,即 O(N)

参考灵神

http://www.lryc.cn/news/620589.html

相关文章:

  • 基于Django的福建省旅游数据分析与可视化系统【城市可换】
  • AI 编程实践:用 Trae 快速开发 HTML 贪吃蛇游戏
  • 【经验分享】如何在Vscode的Jupyter Notebook中设置默认显示行号
  • vscode的wsl环境,ESP32驱动0.96寸oled屏幕
  • 【面板数据】各省及市省级非物质文化遗产数据合集(2005-2024年)
  • 【JavaEE】多线程 -- 初识线程
  • Java应用快速部署Tomcat指南
  • **超融合架构中的发散创新:探索现代编程语言的挑战与机遇**一、引言随着数字化时代的快速发展,超融合架构已成为IT领域的一种重要趋势
  • ts概念讲解
  • 网络原理-HTTP
  • 一致性哈希Consistent Hashing
  • 【代码随想录day 20】 力扣 669. 修剪二叉搜索树
  • 力扣-64.最小路径和
  • 玩转Docker | 使用Docker部署JSON格式化工具ZJSON
  • iOS Sqlite3
  • 磁盘瓶颈现形记 - iostat让I/O压力无所遁形
  • 「iOS」————设计架构
  • iOS 26 一键登录失效:三大运营商 SDK 无法正常获取手机号
  • iOS性能监控新方法多版本对比与趋势分析实战指南
  • iOS混淆工具有哪些?游戏 App 防护下的混淆与加固全攻略
  • 网络通信---Axios
  • iOS App TestFlight 上架全流程案例,从 0 到 1 完成内测分发
  • Docker 部署:Web SSH、RDP、VNC 多协议全能远程管理工具
  • 零基础数据结构与算法——第七章:算法实践与工程应用-搜索引擎
  • 洗浴中心泡池水过滤系统原理深度解析与工程实践
  • 数智先锋 | 告别运维黑盒!豪鹏科技×Bonree ONE构建全栈智能可观测体系
  • 【网络】TCP/UDP总结复盘
  • Ollama如何分别使用2张H100GPU和4张A100部署GPT-OSS-120B全指南:硬件配置与负载均衡实战
  • PostgreSQL——触发器
  • Nginx学习笔记(八)—— Nginx缓存集成