41 数据流中的中位数(时间效率)
题目描述:
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
测试用例:
功能测试(从数据流中读出奇数个数字;从数据流中读出偶数个数字)
边界值测试(从数据流中读出0个、1个、2个数字)
解题思路:
1)push_heap/pop_heap
中位数左边的数据比其小(用最大堆实现左边的数据容器)中位数右边的数据比其大(用最小堆实现右边的数据容器)。往堆中插入一个数据的时间效率是O(logn)。由于只需要O(1)时间就可以得到位于堆顶的数据,因此得到中位数的时间复杂度是O(1)
实现细节:
- 保证数据平均分配到两个堆中。约定偶数个元素时,向右侧(最小堆插入元素)。此时要判断该元素与最大堆中的最大值的关系。要保证最大堆中的所有元素都小于最小堆。
- 注意最大堆是降序排列的,这样最大值才能保存在头部
- 最小堆是升序排列的,头部保存最小值
class Solution { public: void Insert(int num) { int size = maxV.size()+minV.size(); if((size & 0x1) == 0){ //偶数个元素,向最小堆插入元素 //size & 0x1 == 0 //插入的元素要比最大堆的所有元素大 if(maxV.size()>0 && num<maxV[0]){ //先将元素插入最大堆 maxV.push_back(num); //重新排序 push_heap(maxV.begin(),maxV.end(),less<int>()); //最大堆降序排列,头为最大元素 //去除最大元素 num = maxV[0]; //删除最大元素 pop_heap(maxV.begin(),maxV.end(),less<int>()); maxV.pop_back(); } minV.push_back(num); push_heap(minV.begin(),minV.end(),greater<int>()); //最小堆,升序排列,最前面的元素才最小 }else{ //奇数个元素,向最大堆插入元素 //要插入的元素应该比最小堆的所有元素都小 if(minV.size()>0 && num>minV[0]){ minV.push_back(num); push_heap(minV.begin(),minV.end(),greater<int>()); num = minV[0]; pop_heap(minV.begin(),minV.end(),greater<int>()); minV.pop_back(); } maxV.push_back(num); push_heap(maxV.begin(),maxV.end(),less<int>()); } } double GetMedian() { int size = maxV.size()+minV.size(); if(size==0) exit(1); //抛异常 if(size & 0x1 == 1) //奇数个元素 return minV[0]; else return(double(minV[0])+maxV[0])/2; //return (minV[0]+maxV[0])/2; //返回向下取整的数,然后转成double; //return double((minV[0]+maxV[0])/2); //返回向下取整的数,然后转成double; //所有的return都在循环中是否可行,可以。 } private: vector<int> maxV; vector<int> minV; };
求取中位数时:由于元素的类型是int,所以当元素有偶数个时,直接对中间两个元素相加除以二,由于整数除法,会向下取整。如 序列 2,3 中位数会返回2。但实际应该是2.5
return (minV[0]+maxV[0])/2.0; 改成除以2.0;就可以满足条件了。
2)priority_queue
class Solution { priority_queue<int, vector<int>, less<int> > p;//降序,最大堆,左侧 priority_queue<int, vector<int>, greater<int> > q; //升序,最小堆,右侧 public: void Insert(int num){ //根据元素大小,先将元素插入对应的队列(堆)中 if(p.empty() || num <= p.top()) p.push(num); //偶数个元素时,将元素插入最大堆(左侧) else q.push(num); //然后控制两个队列的元素数量最多不能相差1。 //该代码:两个元素相等。p比q多一个元素 //相差两个元素:原本p=q+1,应该向q中添加元素,结果添加到了p中 if(p.size() == q.size() + 2) q.push(p.top()), p.pop(); //原本p=q,应该向p中添加元素,结果向q中添加元素。 if(p.size() + 1 == q.size()) p.push(q.top()), q.pop(); //当添加后,p=q或者p=q+1则不用处理。是正常的添加 } double GetMedian(){ if((p.size()+q.size())==0) exit(1); return p.size() == q.size() ? (p.top() + q.top()) / 2.0 : p.top(); } };
priority_queue用法:
- priority_queue本质是一个堆。头文件是#include<queue>
- 模板申明带3个参数:priority_queue<Type, Container, Functional>,其中Type 为数据类型,Container为保存数据的容器,Functional 为元素比较方式。
- Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector。
- 比较方式默认用operator<,所以如果把后面2个参数缺省的话,优先队列就是大顶堆(降序),队头元素最大。
3)构建一棵"平衡二叉搜索树 "。
每个结点左子树均是小于等于其value的值,右子树均大于等于value值。每个子树均按其 “结点数” 调节平衡。 这样根节点一定是中间值中的一个。若结点数为奇数,则返回根节点的值;若结点个数为偶数,则再从根结点左子数或右子数中个数较多的子树中选出最大或最小值既可。
手写AVL
struct myTreeNode { int val; int count;//以此节点为根的树高 struct myTreeNode* left; struct myTreeNode* right; myTreeNode(int v) : val(v), count(1), left(NULL), right(NULL) {} }; myTreeNode *root = NULL; class Solution { public: /*计算以节点为根的树的高度 */ int totalCount(myTreeNode* node) { if (node == NULL) return 0; else return node->count; } //左左 void rotateLL(myTreeNode* &t) { myTreeNode* k = t->left; myTreeNode* tm = NULL; while (k->right != NULL) { k->count--; tm = k; k = k->right; } if (k != t->left) { k->left = t->left; tm->right = NULL; } t->left = NULL; k->right = t; t->count = totalCount(t->left) + totalCount(t->right) + 1; k->count = totalCount(k->left) + t->count + 1; t = k; } //右右 void rotateRR(myTreeNode* &t) { myTreeNode* k = t->right; myTreeNode* tm = NULL; while (k->left != NULL) { k->count--; tm = k; k = k->left; } if (k != t->right) { k->right = t->right; tm->left = NULL; } t->right = NULL; k->left = t; t->count = totalCount(t->left) + 1; k->count = totalCount(k->right)+ t->count + 1; t = k; } //左右 void rotateLR(myTreeNode* &t) { rotateRR(t->left); rotateLL(t); } //右左 void rotateRL(myTreeNode* &t) { rotateLL(t->right); rotateRR(t); } //插入 void insert(myTreeNode* &root, int x) { if (root == NULL) { root = new myTreeNode(x); return; } if (root->val >= x) { insert(root->left, x); root->count = totalCount(root->left)+ totalCount(root->right) + 1; if (2 == totalCount(root->left) - totalCount(root->right)) { if (x < root->left->val) { rotateLL(root); } else { rotateLR(root); } } } else { insert(root->right, x); root->count = totalCount(root->left)+ totalCount(root->right) + 1; if (2 == totalCount(root->right) - totalCount(root->left)) { if (x > root->right->val) { rotateRR(root); } else { rotateRL(root); } } } } void deleteTree(myTreeNode* root) { if (root == NULL)return; deleteTree(root->left); deleteTree(root->right); delete root; root = NULL; } void Insert(int num) { insert(root, num); } double GetMedian() { int lc = totalCount(root->left), rc = totalCount(root->right); if ( lc == rc) return root->val; else { bool isLeft = lc > rc ; myTreeNode* tmp ; if (isLeft) { tmp = root->left; while (tmp->right != NULL) { tmp = tmp->right; } } else { tmp = root->right; while (tmp->left != NULL) { tmp = tmp->left; } } return (double)(root->val + tmp->val) / 2.0; } } };
补充知识点
- 二叉树搜索可以把插入新数据的平均时间降低到O(logn)。但是,当二叉搜索树极度不平衡从而看起来像一个排序的链表时,插入新数据的时间仍然是O(n)为了得到中位数,可以在二叉树节点中添加一个表示子树节点数目的字段。有了这个字段,可以在平均O(logn)时间内得到中位数,但最差情况仍然需要O(n)。
- 使用平衡的二叉搜索树:AVL树的平衡因子是左右子树的高度差。可以稍做修改,把AVL的平衡因子改为左右子树节点数目之差。 可以用O(;ogn)时间往AVL树中添加一个新的节点,同时用O(1)时间得到所有节点的中位数。 AVL效率很高,但大部分编程语言的函数库都没有实现这个数据结构。