剑指offer 学习笔记 数据流中的中位数
面试题41:数据流中的中位数。如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值;如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
由于数据是从一个数据流中读出来的,数据的数目随时间增加,如果用一个数据容器保存从流中读出的数据,当有新的数据从流中读出来时,将该数据插入容器。重点在于选择容器。
数组是最简单的容器,如果数组没有排序,则可以用Partition函数找出数组中的中位数,因此,在一个没有排序的数组中插入一个数字和找出中位数的时间复杂度分别为O(1)和O(n)。我们还可以在往数组里插入新数据时让数组保持排序,此时可能需要移动O(n)个数,因此插入操作需要O(n)的时间复杂度,但找到一个中位数就简单了,时间复杂度为O(1)。
排序的链表需要O(n)的时间复杂度才能找到合适的位置插入数据,如果定义两个指针指向链表的中间节点(当链表为奇数时两个指针指向同一个节点),那么可以在O(1)的时间内得出中位数。此时插入和查找中位数操作的时间复杂度与使用排序的数组是一样的。
二叉搜索树可以把插入新数据的平均时间降低到O(logn),但当二叉树极度不平衡时,插入新数据的时间仍是O(n)。为了得到中位数,可以在二叉树节点中添加一个表示子树节点数目的字段,有了该字段,就可在平均O(logn)时间内得到中位数,但最差情况需要O(n)的时间。
为了避免二叉搜索树的最差情况,可利用平衡二叉树(AVL)。通常AVL的平衡因子是左右子树的高度差,我们可以将其改为左右子树节点数目之差,这样可以用O(logn)时间往AVL中添加一个新节点,同时用O(1)时间得到所有节点的中位数。
AVL树时间效率很高,但大部分语言的函数库都没实现它。
另一个方法是,将数据按数量分为两份,如果数据有奇数个,则最中间的数左边的数都小于它,右边的数都大于它;而数据有偶数个时,最中间的两个数中左边那个是左边数组中最大的数,右边那个是右边数组中最小的。如果我们能保证数据容器左边的数据都小于右边的数据,那么即使左右两边内部没有排序,也可以根据左边最大的数和右边最小的数来得到中位数。因此,可用最大堆实现左边部分数据容器,最小堆实现右边部分数据容器,往堆中插入一个数据的时间效率为O(logn),而取堆中最值只需O(1)。
总结:
实现最大堆和最小堆的细节:
1.保证数据平均分配到两个堆中,两堆中数据的数目之差不能超过1,为实现平均分配,可以在总数据数量是偶数时把新数据插入最小堆,否则插入最大堆。
2.保证最大堆中的所有数据都要小于最小堆中的最小值,如果数据数量为偶数,应将数据插入最小堆,此时如果这个数据比最大堆中的最大值要小,则应先将这个数字插入到最大堆,接着把最大堆中最大的数字拿出来插入到最小堆中。而当数据总量是奇数时,应将数据插入到最大堆,如果此数据比最小堆中的最小值大,则应先将该数据插入到最小堆,然后把最小堆中的最小值取出来插入到最大堆中。
以下是基于STL的push_heap、pop_heap和vector的实现:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <typename T> class DynamicArray {
public:
void Insert(T num) {
if (((min.size() + max.size()) & 1) == 0) { // 偶数时,应把数字放入最小堆
if (max.size() > 0 && num < max[0]) { // 如果要放入最小堆的数字比最大堆的最大值小,则先将其放入最大堆,size大于0是为了保证max[0]的安全性
max.push_back(num);
push_heap(max.begin(), max.end(), less<T>());
num = max[0]; // 记录下最大堆顶元素值
pop_heap(max.begin(), max.end(), less<T>()); // 将栈顶元素与最后一个元素对调
max.pop_back(); // 删除最大堆中的最大元素
}
min.push_back(num);
push_heap(min.begin(), min.end(), greater<T>());
} else { // 奇数时,应将数字放入最大堆
if (min.size() > 0 && num > min[0]) {
min.push_back(num);
push_heap(min.begin(), min.end(), greater<T>());
num = min[0];
pop_heap(min.begin(), min.end(), greater<T>());
min.pop_back();
}
max.push_back(num);
push_heap(max.begin(), max.end(), less<T>());
}
}
T GetMedian() {
int size = min.size() + max.size();
if (size == 0) {
throw exception("No numbers are available");
}
if (size & 1) {
return min[0];
} else {
return (min[0] + max[0]) / 2;
}
}
private:
vector<T> min, max;
};
int main() {
DynamicArray<int> d;
d.Insert(1);
d.Insert(2);
cout << d.GetMedian() << endl;
d.Insert(3);
cout << d.GetMedian() << endl;
d.Insert(4);
d.Insert(5);
d.Insert(6);
cout << d.GetMedian() << endl;
d.Insert(7);
d.Insert(8);
d.Insert(9);
cout << d.GetMedian() << endl;
}
运行它:
计算两个int的平均数的结果会去掉小数。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)