当前位置: 首页 > 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.dtcms.com/a/330896.html

相关文章:

  • 前端开发如何优化 SEO:常见问题及解决方案全解析​
  • JMeter性能测试详细版(适合0基础小白学习--非常详细)
  • 简单好用的桌面时钟—— Digital Clock 4
  • 飞算JavaAI开发全流程解析:从自然语言到可运行工程的智能进化
  • [Android] 二十四节气日历v1.0.3 - 弘扬传统文化,精致设计,无广告纯净体验!
  • 考研408《计算机组成原理》复习笔记,第五章(3)——CPU的【数据通路】
  • 双机热备实验
  • Linux编程1:进程和线程
  • DTW算法判断两条曲线的相似度,Python代码
  • [激光原理与应用-272]:理论 - 波动光学 - 单纯的白色其实并不单纯
  • 迅为RK3562开发板获取Android13源码
  • 3D商品展示:技术狂欢下的普及困局
  • 【新启航】起落架大型结构件深孔检测探究 - 激光频率梳 3D 轮廓检测
  • Uniapp 自定义头部导航栏
  • 表达式树实战:Unity动态逻辑编程
  • 考研408《计算机组成原理》复习笔记,第三章(6)——Cache(超级重点!!!)
  • 【科研绘图系列】R语言绘制蝶形条形图蝶形柱状堆积图
  • 考研408《计算机组成原理》复习笔记,第四章(3)——指令集、汇编语言
  • 一致性哈希Consistent Hashing
  • Rust Web框架Axum学习指南之入门初体验
  • Java面试宝典:JVM性能优化
  • 【代码随想录day 20】 力扣 669. 修剪二叉搜索树
  • MySQL 性能优化实战指南:释放数据库潜能的艺术
  • 【visual studio】visual studio配置环境opencv和onnxruntime
  • 零知开源——基于STM32F4的HC-12无线通信系统及ST7789显示应用
  • 【Linux】库制作与原理
  • mysql卸载了 服务内还显示如何解决
  • CVPR 2025丨时间序列:动态多尺度机制登场,即插即用,预测稳准狠刷新SOTA
  • Vivado GPIO详解
  • 量化因子RSI