【数据结构与算法】堆排序

👨‍💻 关于作者:会编程的土豆

"不是因为看见希望才坚持,而是坚持了才看见希望。"

你好,我是会编程的土豆,一名热爱后端技术的Java学习者。

📚 正在更新中的专栏:

💕作者简介:后端学习者

例题例题

cpp 复制代码
#include <queue>
#include <functional>
using namespace std;

class MedianFinder {
private:
    // 大顶堆:存较小的一半数字(堆顶是这半的最大值)
    priority_queue<int> maxHeap;
    
    // 小顶堆:存较大的一半数字(堆顶是这半的最小值)
    priority_queue<int, vector<int>, greater<int>> minHeap;
    
public:
    MedianFinder() {
        // 构造函数,什么都不用做
    }
    
    void addNum(int num) {
        // 核心规则:
        // 1. 新数先进大顶堆(较小的一半)
        // 2. 把大顶堆的最大值移到小顶堆
        // 3. 如果小顶堆比大顶堆大,把小顶堆的最小值移回大顶堆
        
        maxHeap.push(num);
        minHeap.push(maxHeap.top());
        maxHeap.pop();
        
        // 保持平衡:maxHeap 的大小要么等于 minHeap,要么比 minHeap 多 1
        if (maxHeap.size() < minHeap.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;
        }
    }
};

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder* obj = new MedianFinder();
 * obj->addNum(num);
 * double param_2 = obj->findMedian();
 */

数据流中位数(MedianFinder)详解:为什么要用两个堆?

这道题很多人第一次看到会有点懵:

数据是不断加入的,每次都要快速返回中位数

如果你用最直接的方法:

  • 每次插入后排序

  • 再取中位数

复杂度是:O(n log n)(排序),明显不行。


一、核心问题

我们要解决的是:

如何在"动态插入数据"的情况下,快速找到中位数?


二、关键思路:把数据分成两半

我们可以这样想:

  • 一半较小的数

  • 一半较大的数

中位数就在这两部分的"交界处"。


三、为什么用两个堆?

我们用两个堆来维护:

1. 大顶堆(maxHeap)

  • 存较小的一半

  • 堆顶是"较小部分的最大值"


2. 小顶堆(minHeap)

  • 存较大的一半

  • 堆顶是"较大部分的最小值"


这样:

  • 如果总数是奇数 → 中位数就是 maxHeap.top()

  • 如果是偶数 → 中位数是两个堆顶的平均


四、关键难点:如何维持平衡?

必须保证:

  1. maxHeap.size() == minHeap.size()

  2. maxHeap.size() == minHeap.size() + 1

也就是说:

大顶堆最多比小顶堆多一个元素


五、addNum 的核心逻辑(重点)

来看代码:

cpp 复制代码
void addNum(int num) {
    maxHeap.push(num);
    minHeap.push(maxHeap.top());
    maxHeap.pop();

    if(maxHeap.size() < minHeap.size())
    {
        maxHeap.push(minHeap.top());
        minHeap.pop();
    }
}

这段代码其实非常巧妙,可以拆成三步理解:


第一步:先把新元素丢进大顶堆

复制代码
maxHeap.push(num);

先不管大小,直接放进去。


第二步:把最大值丢到小顶堆

复制代码
minHeap.push(maxHeap.top());
maxHeap.pop();

这一步的作用是:

保证 minHeap 里全是"较大的数"


第三步:重新平衡两个堆

cpp 复制代码
if(maxHeap.size() < minHeap.size())
{
    maxHeap.push(minHeap.top());
    minHeap.pop();
}

如果小顶堆多了,就把最小的那个"较大值"搬回来。


六、为什么这样一定正确?

你可以这样理解整个过程:

每次插入:

  1. 先进入"小的那一半"(maxHeap)

  2. 再把最大值送去"大的一半"(minHeap)

  3. 最后调整数量

这样可以保证:

  • maxHeap 里永远是较小的一半

  • minHeap 里永远是较大的一半

而且顺序始终正确。


七、findMedian 就很简单了

cpp 复制代码
double findMedian() {
    if(maxHeap.size() > minHeap.size())
    {
        return maxHeap.top();
    }
    else
    {
        return (maxHeap.top() + minHeap.top()) / 2.0;
    }
}

两种情况:

1. 奇数个
复制代码
maxHeap 多一个

中位数就是:

复制代码
maxHeap.top()

2. 偶数个
复制代码
两个堆一样多

中位数:

复制代码
(maxHeap.top() + minHeap.top()) / 2

八、时间复杂度

  • 插入:O(log n)

  • 查询中位数:O(1)

相比排序,效率提升非常明显。


九、常见错误

1. 顺序写反

很多人会写成:

复制代码
minHeap.push(num);

这样会破坏结构。


2. 没有保持大小关系

必须保证:

复制代码
maxHeap.size() >= minHeap.size()

3. 忘记平衡

少了这一步就会错:

复制代码
if(maxHeap.size() < minHeap.size())

十、总结一句话

这道题的本质就是:

用两个堆维护"中位数左右两边",保证数量平衡


如果你再往上提升,这道题还有进阶玩法:

  • 支持删除(变成滑动窗口中位数)

  • 使用 multiset / 平衡树实现

  • 手写堆优化

这些在面试中也非常常见。

相关推荐
查古穆2 小时前
二分查找-搜索二维矩阵
算法
会编程的土豆2 小时前
【数据结构与算法】希尔排序
数据结构·c++·算法·排序算法
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(二十五)——Alpha混合与透明效果:分层窗口实战
c++·windows·学习·图形渲染·win32
charlie1145141912 小时前
通用GUI编程技术——图形渲染实战(二十四)——GDI Region与裁切:不规则窗口与可视化控制
c++·windows·学习·c·图形渲染·win32
cch89182 小时前
五大PHP框架对比:如何选择最适合你的?
开发语言·php
9分钟带帽2 小时前
vscode中配置Qt6和CMake的开发环境
c++·vscode·cmake
南 阳2 小时前
Python从入门到精通day62
开发语言·python
邦爷的AI架构笔记2 小时前
GLM-5.1 接入踩坑记录:用免费开源模型搭个 AI 代码审计小工具
后端·算法
苏宸啊2 小时前
哈希扩展问题
算法·哈希算法