leetcode 295. 数据流的中位数

时间复杂度分析:为什么你的中位数查找方案会超时?

分析你提供的MedianFinder实现,其时间复杂度较高的原因主要在于findMedian函数的实现方式。让我详细解释:

代码时间复杂度分析

你的代码中两个主要函数的时间复杂度如下:

  1. addNum函数

    void addNum(int num) {nums.emplace_back(num);
    }
    
    • 时间复杂度:O(1)
    • 解释:将数字添加到vector末尾,是常数时间操作
  2. findMedian函数

    double findMedian() {if (nums.size() <= 0) {return 0.0;}sort(nums.begin(), nums.end()); // 关键性能瓶颈auto n = nums.size();if (n % 2) {return nums[n / 2];} else {return (nums[n / 2] + nums[(n - 1) / 2]) / 2.0;}
    }
    
    • 时间复杂度:O(n log n),其中n是当前数字的个数
    • 解释:每次查找中位数时都对整个数组进行排序,排序的时间复杂度是O(n log n)

为什么这个时间复杂度无法通过?

LeetCode 295题「寻找中位数」的测试用例通常包含大量操作。假设:

  • 进行m次addNum操作和m次findMedian操作
  • 你的实现总时间复杂度为O(m * n log n),其中n是数据规模

当m和n较大时(例如10^5次操作),这样的复杂度会导致超时。具体来说:

  • 对于10^5次操作,O(n log n)的复杂度意味着约10^5 * 10^5 log 10^5 ≈ 1010次操作,这远远超过了LeetCode的时间限制(通常要求算法在108次操作内完成)

优化方案:使用双堆实现

为了将时间复杂度优化到更高效的水平,我们可以使用两个堆(大顶堆和小顶堆)来维护数据:

  • 大顶堆maxHeap保存较小的一半数字
  • 小顶堆minHeap保存较大的一半数字
  • 确保两个堆的大小差不超过1,这样堆顶元素就是中位数
class MedianFinder {
private:priority_queue<int> maxHeap; // 大顶堆,保存较小的一半数字priority_queue<int, vector<int>, greater<int>> minHeap; // 小顶堆,保存较大的一半数字public:MedianFinder() {}void addNum(int num) {// 平衡两个堆的大小if (maxHeap.empty() || num <= maxHeap.top()) {maxHeap.push(num);} else {minHeap.push(num);}// 调整堆的大小,使两个堆的大小差不超过1if (maxHeap.size() > minHeap.size() + 1) {minHeap.push(maxHeap.top());maxHeap.pop();} else if (minHeap.size() > maxHeap.size()) {maxHeap.push(minHeap.top());minHeap.pop();}}double findMedian() {if (maxHeap.size() > minHeap.size()) {return maxHeap.top();} else {return (maxHeap.top() + minHeap.top()) / 2.0;}}
};

优化后时间复杂度分析

  • addNum函数:O(log n)

    • 堆的插入和弹出操作都是O(log n)
    • 每次添加数字后最多进行一次堆调整
  • findMedian函数:O(1)

    • 直接返回堆顶元素,无需排序

空间复杂度

  • O(n),其中n是存储的数字个数
  • 需要两个堆来存储数据

这种双堆实现能够高效处理大量的添加和查询操作,轻松通过LeetCode的时间限制。关键在于避免了每次查询都进行排序,而是通过堆结构维护数据的有序性。

在C++中,小顶堆(最小堆)的确可以通过std::priority_queue结合std::greater<int>来实现。下面详细解释其原理和用法:

一、小顶堆与greater<int>的关系

1. 堆的基本概念
  • 是一种完全二叉树,分为两种:
    • 大顶堆:每个节点的值都大于或等于其子节点的值,堆顶是最大值。
    • 小顶堆:每个节点的值都小于或等于其子节点的值,堆顶是最小值。
2. std::priority_queue的模板参数

std::priority_queue的默认实现是大顶堆,其模板定义为:

template <class T,class Container = vector<T>,class Compare = less<T>
> class priority_queue;
  • T:元素类型。
  • Container:存储元素的容器(默认用vector)。
  • Compare:比较函数(默认用less<T>,即大顶堆)。
3. 小顶堆的实现

Compare参数改为greater<T>即可实现小顶堆:

std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
  • greater<int>是一个函数对象,用于比较两个整数,当a > b时返回true,因此堆会按升序排列,堆顶是最小值。

二、大顶堆与小顶堆的对比

特性大顶堆(默认)小顶堆(greater)
模板参数priority_queue<int>priority_queue<int, vector<int>, greater<int>>
堆顶元素最大值最小值
插入操作保持堆顶为最大值保持堆顶为最小值
应用场景取最大值、任务调度(高优先级)取最小值、最小生成树算法

三、小顶堆的常用操作示例

#include <iostream>
#include <queue>
#include <vector>int main() {// 创建小顶堆std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;// 插入元素minHeap.push(5);minHeap.push(1);minHeap.push(3);minHeap.push(2);minHeap.push(4);// 堆顶是最小值std::cout << "堆顶元素(最小值): " << minHeap.top() << std::endl;  // 输出: 1// 弹出堆顶元素minHeap.pop();std::cout << "弹出后堆顶元素: " << minHeap.top() << std::endl;     // 输出: 2// 查看堆的大小std::cout << "堆大小: " << minHeap.size() << std::endl;            // 输出: 4// 注意:priority_queue不支持随机访问,只能通过弹出操作遍历while (!minHeap.empty()) {std::cout << minHeap.top() << " ";minHeap.pop();}// 输出: 2 3 4 5std::cout << std::endl;return 0;
}

四、在中位数查找中的应用

在LeetCode 295题中,小顶堆用于保存较大的一半数字,大顶堆保存较小的一半数字:

class MedianFinder {
private:std::priority_queue<int> maxHeap;          // 大顶堆,保存较小的一半std::priority_queue<int, vector<int>, greater<int>> minHeap;  // 小顶堆,保存较大的一半public:void addNum(int num) {if (maxHeap.empty() || num <= maxHeap.top()) {maxHeap.push(num);} else {minHeap.push(num);}// 调整堆大小平衡if (maxHeap.size() > minHeap.size() + 1) {minHeap.push(maxHeap.top());maxHeap.pop();} else if (minHeap.size() > maxHeap.size()) {maxHeap.push(minHeap.top());minHeap.pop();}}double findMedian() {if (maxHeap.size() > minHeap.size()) {return maxHeap.top();} else {return (maxHeap.top() + minHeap.top()) / 2.0;}}
};
  • 小顶堆的作用:确保较大的一半数字按升序排列,堆顶是其中最小值。
  • 大顶堆的作用:确保较小的一半数字按降序排列,堆顶是其中最大值。

五、greater<int>的底层实现

std::greater<int>是一个函数对象(仿函数),其operator()定义如下:

struct greater {bool operator()(const int& x, const int& y) const {return x > y;}
};
  • x > y时返回true,因此在priority_queue中,较小的元素会被推到堆顶,形成小顶堆。

总结

  • 小顶堆的实现:使用priority_queue<int, vector<int>, greater<int>>
  • 核心原理:通过greater<int>改变元素的比较方式,使堆顶始终为最小值。
  • 应用场景:需要频繁获取最小值的场景,如优先队列、中位数查找、最小生成树算法等。

掌握小顶堆的实现和原理,能帮助你在算法题中高效解决需要维护有序数据的数据结构问题。

双堆实现中位数查找时堆平衡条件的解释

在使用双堆(大顶堆和小顶堆)实现中位数查找器时,堆平衡条件的设计是确保能正确获取中位数的关键。让我解释为什么在判断堆大小时,两个条件存在差异:

双堆实现的基本原理

在中位数查找器中,我们使用:

  • 大顶堆 maxHeap 保存较小的一半数字
  • 小顶堆 minHeap 保存较大的一半数字

为了确保能正确获取中位数,需要满足:

  • 当总数字个数为奇数时,大顶堆的大小比小顶堆大1,此时大顶堆顶就是中位数
  • 当总数字个数为偶数时,两个堆大小相等,此时中位数是两个堆顶的平均值

堆平衡条件分析

if (maxHeap.size() > minHeap.size() + 1) {minHeap.push(maxHeap.top());maxHeap.pop();
} else if (minHeap.size() > maxHeap.size()) {maxHeap.push(minHeap.top());minHeap.pop();
}
为什么大顶堆的条件是 maxHeap.size() > minHeap.size() + 1
  • 大顶堆应该保存较小的一半数字,当总数字个数为奇数时,大顶堆大小应比小顶堆大1
  • 当大顶堆的大小超过小顶堆大小1以上时(即maxHeap.size() >= minHeap.size() + 2),需要调整
  • 例如:大顶堆有5个元素,小顶堆有3个元素,差值为2,需要调整
为什么小顶堆的条件是 minHeap.size() > maxHeap.size()
  • 当总数字个数为偶数时,两个堆大小应相等
  • 当小顶堆的大小超过大顶堆时(即minHeap.size() >= maxHeap.size() + 1),需要调整
  • 例如:小顶堆有4个元素,大顶堆有3个元素,差值为1,需要调整

关键差异的原因

大顶堆允许的最大差值是1,小顶堆不允许任何差值
  • 大顶堆可以比小顶堆多1个元素(当总元素个数为奇数时)
  • 小顶堆不能比大顶堆多元素,最多只能相等(当总元素个数为偶数时)
示例说明

假设当前大顶堆和小顶堆的大小分别为:

  • 情况1:maxHeap.size() = 5, minHeap.size() = 3

    • 差值为2,满足maxHeap.size() > minHeap.size() + 1(5 > 3+1),需要调整
    • 调整后:maxHeap.size() = 4, minHeap.size() = 4
  • 情况2:maxHeap.size() = 3, minHeap.size() = 4

    • 满足minHeap.size() > maxHeap.size()(4 > 3),需要调整
    • 调整后:maxHeap.size() = 4, minHeap.size() = 3
  • 情况3:maxHeap.size() = 4, minHeap.size() = 3

    • 差值为1,满足奇数个元素的条件,无需调整
    • 中位数为大顶堆顶元素
  • 情况4:maxHeap.size() = 3, minHeap.size() = 3

    • 满足偶数个元素的条件,无需调整
    • 中位数为两个堆顶元素的平均值

总结

堆平衡条件的设计基于以下原则:

  1. 大顶堆可以比小顶堆多1个元素(奇数个元素时)
  2. 小顶堆不能比大顶堆多元素,最多相等(偶数个元素时)
  3. 当大顶堆大小超过小顶堆大小1以上时需要调整(+1的由来)
  4. 当小顶堆大小超过大顶堆时需要调整(不需要+1,因为不允许小顶堆更大)

这种条件设计确保了:

  • 大顶堆始终保存较小的一半数字
  • 小顶堆始终保存较大的一半数字
  • 可以在O(1)时间内获取中位数

理解这些条件背后的原理,有助于更好地掌握双堆数据结构在中位数查找中的应用。

你修改后的代码在每次插入新元素时立即排序,虽然简化了中位数查找的逻辑,但整体效率仍然不如双堆实现。这是因为排序操作的时间复杂度远高于堆操作。以下是详细分析:

一、时间复杂度对比

1. 你的修改版本
  • 插入操作 addNum
    每次插入后排序整个数组,时间复杂度为 O(n log n),其中 n 是当前元素数量。
    (注:即使使用二分查找找到插入位置,移动元素的时间复杂度仍为 O(n),整体仍为 O(n)。)

  • 查询操作 findMedian
    直接访问中间元素,时间复杂度为 O(1)

2. 双堆实现
  • 插入操作 addNum
    维护两个堆的平衡,时间复杂度为 O(log n)
    (堆的插入/删除操作时间为 O(log n),调整平衡最多两次堆操作。)

  • 查询操作 findMedian
    直接访问堆顶元素,时间复杂度为 O(1)

3. 性能差异

假设进行 m 次插入和查询,总时间复杂度:

  • 你的实现:O(m × n log n)
    每次插入都需排序整个数组,n 随插入次数增长,最终接近 m。
  • 双堆实现:O(m × log n)
    每次插入仅需对数时间维护堆平衡。

二、为什么排序效率低?

排序操作(如快速排序、归并排序)的时间复杂度为 O(n log n),这在数据量大时非常昂贵。即使你每次插入后立即排序,避免了重复排序的开销,但单次排序的代价仍然远高于堆的对数时间操作

三、示例对比

假设已有 10,000 个元素,插入第 10,001 个元素:

  • 你的实现
    对 10,001 个元素排序,需执行约 10,001 × log₂(10,001) ≈ 130,000 次操作
  • 双堆实现
    调整两个堆的平衡,最多需执行 2 × log₂(10,001) ≈ 26 次操作

随着数据量增长,排序的开销呈指数级上升,而堆操作始终保持对数时间。

四、优化方向:插入排序(仍不如双堆)

如果你希望保持数组有序,可以使用插入排序(二分查找定位插入位置),将插入时间优化到 O(n)

class MedianFinder {
private:vector<int> nums;public:void addNum(int num) {// 二分查找插入位置auto it = lower_bound(nums.begin(), nums.end(), num);nums.insert(it, num);  // 插入操作 O(n)}double findMedian() {int n = nums.size();return n % 2 ? nums[n/2] : (nums[n/2] + nums[n/2-1]) / 2.0;}
};

但插入操作的 O(n) 仍劣于双堆的 O(log n),尤其是在数据量大时。

五、双堆实现的核心优势

  1. 动态维护有序性
    堆结构通过 O(log n) 时间调整,始终保持堆顶元素为中位数的候选者。

  2. 避免全局排序
    不需要对所有元素排序,仅维护两个堆的局部有序性,节省大量计算资源。

  3. 适用于流数据
    每次插入和查询的时间复杂度稳定,即使处理海量数据流也能保持高效。

总结

你的修改版本虽然简化了代码逻辑,但每次插入后的排序操作成为性能瓶颈。双堆实现通过局部有序性对数时间操作,在动态数据场景下显著优于全局排序方案。这体现了算法设计中数据结构选择的重要性——合适的数据结构(如堆)能将问题的时间复杂度从 O(n log n) 优化到 O(log n)。

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

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

相关文章

大语言模型介绍

随着2022年底 ChatGPT 再一次刷新 NLP 的能力上限&#xff0c;大语言模型&#xff08;Large Language Model&#xff0c;LLM&#xff09;开始接替传统的预训练语言模型&#xff08;Pre-trained Language Model&#xff0c;PLM&#xff09; 成为 NLP 的主流方向&#xff0c;基于…

STM32 CCR寄存器

​1. CCR寄存器在输入捕获模式下的特性​ ​只读属性​&#xff1a; 当定时器通道配置为输入捕获模式&#xff08;如捕获上升沿/下降沿&#xff09;时&#xff0c;CCR寄存器硬件自动变为只读​。软件写入操作无效&#xff0c;只能在捕获事件发生时由硬件自动更新为当前CNT值。…

【JS-6-ES6中的let和const】深入理解ES6中的let和const:块级作用域与变量声明的新范式

在ES6(ECMAScript 2015)之前&#xff0c;JavaScript中只有var一种变量声明方式&#xff0c;这导致了许多作用域相关的问题。ES6引入了let和const两种新的变量声明方式&#xff0c;彻底改变了JavaScript的作用域规则。本文将深入探讨let和const的特性、优势以及它们与var的区别。…

[C语言]数据类型关键字详解

基本数据类型 关键字说明存储大小(通常)取值范围(通常)示例int声明整型变量4字节(32位系统)-2,147,483,648 到 2,147,483,647int count 100;char声明字符型变量1字节-128 到 127 或 0 到 255char grade ‘A’;float声明单精度浮点数4字节1.2e-38 到 3.4e38 (约6-7位有效数字…

黑马python(二十二)

目录&#xff1a; 1.Python操作Mysql基础使用 2.Python操作Mysql数据插入 3.综合案例 1.Python操作Mysql基础使用 2.Python操作Mysql数据插入 3.综合案例 代码复用 黑马python&#xff08;二十一&#xff09;章节的的代码&#xff0c;读取文件内容

课堂笔记:吴恩达的AI课(AI FOR EVERYONE)-W1 深度学习的非技术性解释

深度学习的非技术性解释 &#xff08;1&#xff09;示例1&#xff1a;以商场为主买T恤为例&#xff0c;价格和需求的关系怎么样&#xff1f; 一般来说&#xff0c;价格越高&#xff0c;需求越少 这里输入A是 价格&#xff0c;输出B是需求&#xff0c;其中的映射关系是神经元&a…

dlib检测视频中的人脸并裁剪为图片保存

环境要求 找个带有基本cv配置的虚拟环境安装上dlib依赖的人脸检测的基础环境即可&#xff0c;主要是&#xff1a; pip install boost dlib opencv-python缺的按提示安装。 demo 设置好视频路径和图像保存路径&#xff0c;裁剪尺寸&#xff08;默认256&#xff09;以及裁剪帧…

真的!ToDesk远程控制已上线原生鸿蒙系统!

2025年5月&#xff0c;ToDesk远程控制正式宣布完成对PC鸿蒙系统的适配&#xff0c;成为业界首批原生支持HarmonyOS OS的跨端远控工具。 作为国内支持上亿设备的远程控制软件&#xff0c;ToDesk以无缝互联、快速响应、安全无界为核心&#xff0c;重新定义了跨设备远程协作的界限…

Java-58 深入浅出 分布式服务 ACID 三阶段提交3PC 对比2PC

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月16日更新到&#xff1a; AI炼丹日志-29 - 字节…

matplotlib 绘制饼图

1、功能介绍&#xff1a; 使用 python 的 matplotlib 库来创建一个简单的饼图。 2、代码部分&#xff1a; import matplotlib.pyplot as plt# 示例数据 labels [A, B, C, D, E] # 类别标签 sizes [15, 30, 45, 5, 5] # 每个类别对应的数值&#xff08;百分比&#xff09…

用Rust写平衡三进制除法器

1、除法的本质 除法的本质是减法&#xff0c;也就是一个大的数减去一个小的数&#xff0c;比如:10/2&#xff0c;也就是10-2-2-2-2-20&#xff0c;所以商5余0&#xff0c;10/3&#xff0c;也就是10-3-3-31&#xff0c;所以商3余1&#xff0c;这也是很常见的方法&#xff0c;但如…

深入探索WordPress Multisite:构建与管理多站点网络

随着互联网的快速发展&#xff0c;越来越多的企业和个人开始使用内容管理系统来搭建和维护自己的网站。WordPress作为全球最受欢迎的CMS之一&#xff0c;因其强大的功能和灵活性&#xff0c;成为了许多网站管理员的首选平台。而在一些特定需求的场景下&#xff0c;WordPress Mu…

.Net Core 获取文件路径

在 .NET Core 中获取文件路径的方法取决于你要获取的文件的位置和上下文。这里将介绍几种常见的方式来获取文件路径。 1. 获取当前工作目录 你可以使用 Directory.GetCurrentDirectory() 方法来获取当前工作目录的路径&#xff1a; using System; using System.IO; class P…

顺序表整理和单项链表01 day20

二&#xff1a;各个主要函数 一&#xff1a;CreatSeqList SeqList *CreateSeqList(int len); -------------------------------------------------------------/*** brief Create a Seq List object 创建一个顺序表** param n 是顺序表的大小* return SeqList* 指向顺序表的…

电商导购app平台的缓存策略与性能优化方案:架构师的实践经验

电商导购app平台的缓存策略与性能优化方案&#xff1a;架构师的实践经验 大家好&#xff0c;我是阿可&#xff0c;微赚淘客系统及省赚客APP创始人&#xff0c;是个冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 缓存策略的重要性 在电商导购APP平台中&#xff…

学习C++、QT---12(C++的继承、权限对继承的影响)

每日一言 你的价值&#xff0c;由你自己定义&#xff0c;无需他人评判。 C的继承 直接上案例 继承是什么意思呢&#xff0c;就是我本来这个类我叫他基类、我希望创建我的下一个类有我这之前的类的属性和方法&#xff0c;那么我如果不用继承的话&#xff0c;就需要多写很多一样…

(6)Wireshark的TCP包详解-上篇

1.简介 上一篇中通过介绍和讲解&#xff0c;应该知道要讲解和介绍的内容在哪里了吧&#xff0c;没错就是介绍OSI七层模型的传输层。因为只有它建立主机端到端的连接如&#xff1a;TCP、UDP。 2.TCP是什么? tcp是工作在传输层&#xff0c;也就是网络层上一层的协议。 它是面…

太极八卦罗盘JS绘制

LeaferJS 是一款好用的 Canvas 引擎,通过LeaferJS绘制罗盘案例. https://www.leaferjs.com/ui/guide/ 示例 太极八卦罗盘 直接上代码 <template><div id"LuoPan"></div><div id"info"><p>屏幕宽度: {{ screenWidth }}px<…

Python开源项目月排行 2025年5月

#2025年5月2025年6月1日1scrapy一个开源的、基于 Python 的高性能网络爬虫和数据抓取框架。Scrapy 项目最初由伦敦的网络聚合和电子商务公司 Mydeco 的员工以及乌拉圭蒙得维的亚的网络咨询公司 Insophia 的开发者共同创建。目前&#xff0c;Scrapy 由 Zyte&#xff08;原名 Scr…

Debezium日常分享系列之:在 Kubernetes 中使用 Debezium 的 CDC

Debezium日常分享系列之&#xff1a;在 Kubernetes 中使用 Debezium 的 CDC 架构源数据库创建数据库凭证密钥Debezium 自定义镜像构建并推送镜像Kafka Connect 集群Debezium Postgres 连接器Debezium 创建的 Kafka 主题 Debezium 是一个开源的分布式变更数据捕获 (CDC) 平台。D…