剑指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的平均数的结果会去掉小数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值