堆的概念
堆是一个完全二叉树,堆中的每一个节点的值都必须大于等于(或者小于等于)其子树中每个节点的值。
大顶堆:每个节点的值都大于等于子树中每个节点值的堆
小顶堆:每个节点的值都小于等于子树中每个节点值的堆
堆的存储:使用数组存储比较节省空间
堆的操作
堆化
1、插入数据
自下而上,将数据插入到末尾,然后与父节点比较并进行调整,直到合适或者达到顶点
2、删除堆顶元素
自上而下,将末尾的数据移动到堆顶,然后与左右子节点进行比较并调整,直到合适或者到达叶子节点
class Heap {
private:
int * a; //数组,从下标1开始存储数据
int size; //堆可以存储的最大数据个数
int count; //堆中已经存储的数据个数
public:
Heap(int n) {
a = new int[n + 1];
size = n;
count = 0;
}
~Heap() {
delete[] a;
}
void Insert(int data); //往堆中插入一个元素
void RemoveMax(); //从堆顶删除元素
}
//往堆中插入一个元素
void Heap::Insert(int data) {
if (count >= size) {
cout << "heap is full, insert failed" << endl;
return;
}
++count;
a[count] = data;
int i = count;
//自下往上进行堆化
while (i / 2 > 0 && a[i] > a[i / 2]) {
int tmp = a[i / 2];
a[i / 2] = a[i];
a[i] = tmp;
i = i / 2;
}
}
//从堆顶删除元素
void Heap::RemoveMax() {
if (count == 0) {
cout << "headp is empty, remove failed" << endl;
return;
}
a[1] = a[count];
count--;
Heapify(a, count, 1);
}
//自上往下堆化
void Heap::Heapify(int* a, int count, int index) {
while (true) {
int maxpos = index;
if ((2 * index + 1 <= count) && (a[2 * index + 1] > a[index])) {
maxpos = 2 * index + 1;
}
if ((2 * index <= count) && (a[2 * index] > a[maxpos])) {
maxpos = 2 * index;
}
// 如果maxpos没有更新则退出
if (maxpos == index ) break;
// swap a[index] and a[maxpos]
int tmp = a[index];
a[index] = a[maxpos];
a[maxpos] = tmp;
index = maxpos;
}
}
基于堆进行排序
1、建堆
思路1: 初始时堆中只包含1个数据,然后将下标从2到n的数据依次插入到堆中(从下往上堆化)
思路2:从最后一个非叶子节点开始,依次从上往下进行堆化
时间复杂度o(n),每个节点堆化过程中需要比较和交换的节点个数跟这个节点的高度k成正比
2、排序
将数组第一个元素与最后一个元素交换,剩下n-1个元素重新堆化,完成后再取堆顶元素放到n-1位置,重复到堆中只剩下下标为1的一个元素
时间复杂度o(nlogn),原地排序算法,不稳定,因为存在将堆的最后一个节点跟堆顶节点互换操作,可能改变值相同数据的原始相对顺序
3、堆排序为什么没有快排好?
(1)数据访问没有快排好,快排数据是顺序访问的,堆排序是跳着访问,堆CPU缓存不友好
(2)同样的数据,堆排序算法数据交换次数要多于快排,快排交换次数不会比逆序度多,堆排序建堆打乱原顺序,有序数据建堆后变无序
//堆排序
void HeapSort(int * a, int n) {
//建堆
buildHeap(a, n);
int k = n;
while(k > 1) {
//将堆顶元素与最后一个进行交换
swap(a, 1, k);
--k;
//调整堆
heapify(a, k, 1);
}
for (int i = 1; i <= n; i++) {
cout << a[i] << " ";
}
cout << endl;
}
//建堆
void buildHeap(int * a, int n) {
for (int i = n / 2; i >= 1; --i) {
//从第一个非叶子节点到堆顶元算逐一进行自上而下的堆化调整
heapify(a, n, i);
}
}
void heapify(int * a, int n, int index) {
while(true) {
int maxpos = index;
if ((index * 2 <= n) && (a[index * 2] > a[maxpos])) {
maxpos = index * 2;
}
if ((index * 2 + 1 <= n) && (a[index * 2 + 1] > a[maxpos])) {
maxpos = index * 2 + 1;
}
if (maxpos == index) {
break;
}
swap(a, index, maxpos);
index = maxpos;
}
}
堆的应用
优先级队列
不同语言中应用
java的PriorityQueue,C++的priority_queue
合并有序小文件
有100个文件,每个文件大小100MB文件内有序
从这100个里面取出字符放到小顶堆中,堆顶元素是最小的,删除,然后再从小文件总取下一个字符,循环。
高性能定时器
按照任务设定的执行时间,存储在优先级队列中。
队首存储的是最先执行的任务。
这样定时器不用每隔1秒扫描一遍,直接拿到队首任务计算间隔时间T,T秒之后再执行,然后调整堆,再计算新队首任务时间,不用轮询,不用遍历整个任务表,性能提高。
求TopK系列
求TOP-K
可以使用堆来实现:维护一个大小为k的小顶堆,顺序遍历数组数据,与堆顶元素比较,如果比堆顶大就删除堆顶,插入该数组数据;如果比堆顶小,不处理。遍历完后就是前K大数据。
#include<iostream>
using namespace std;
class MinHeap {
private:
int * a; //数组,从下标1开始存储数据
int size; //堆可以存储的最大数据个数
int count; //堆中已经存储的数据个数
public:
MinHeap(int n) {
a = new int[n + 1];
size = n;
count = 0;
}
~MinHeap() {
delete[] a;
}
void Insert(int data); //往堆中插入一个元素
void Print();
void Swap(int * a, int index1, int index2);
};
//往堆中插入一个元素
void MinHeap::Insert(int data) {
//如果堆还没有满,则插入
if (count < size) {
++count;
a[count] = data;
int i = count;
//自下往上进行堆化
while (i / 2 > 0 && a[i] < a[i / 2]) {
cout << a[i] << " " << a[i/2] << endl;
int tmp = a[i / 2];
a[i / 2] = a[i];
a[i] = tmp;
i = i / 2;
}
} else {
// 如果堆满,判断跟堆顶元素的大小关系,比堆顶大则插入,比其小则忽略
if (data < a[1]) return;
a[1] = data;
//自上而下进行堆化
int index = 1;
int maxpos = 1;
while(true) {
maxpos = index;
if ((2 * index) <= count && (a[2 * index] < a[maxpos])) {
maxpos = 2 * index;
}
if ((2 * index + 1 <= count) && (a[2 * index + 1] < a[maxpos])) {
maxpos = 2 * index + 1;
}
if (maxpos == index){
break;
}
Swap(a, maxpos, index);
index = maxpos;
}
}
}
int main() {
int a[10] = {0, 4, 9, 1, 2, 7, 6, 8, 3, 5};
MinHeap * heap = new MinHeap(5);
for (int i = 0; i < 10; i++) {
heap->Insert(a[i]);
}
heap->Print();
}
求中位数
方法:
1、维护两个堆,一个大顶堆,一个小顶堆。大顶堆存储前半部分数据,小顶堆存储后半部分数据,且小顶堆中的数据都大于大顶堆中的数据。
2、如果新加入的数据小于等于大顶堆堆顶元素,就将数据插入到大顶堆,否则就插入到小顶堆
3、如果不满足大顶堆n/2+1 or n/2、小顶堆n/2的个数要求,则从一个堆的堆顶元素移动到另一个堆中
说明:插入时间复杂度o(logn),求中位数时间复杂度o(1)
求其他百分位数据,原理与求中位数类似,比如求99%的数据,大顶堆保存99%数据,小顶堆保存1%数据,大顶堆堆顶数据是所求。
代码实现:
方法一:使用上述求TOPK的代码思路,生成一个大顶堆,一个小顶堆。
方法二:使用c++中的priority_queue优先级队列
优先队列是c++中的一种重要数据结构,它由二项队列编写而成的,可以以O(log n) 的效率查找一个队列中的最大值或者最小值,其中是最大值还是最小值是根据创建的优先队列的性质来决定的。
声明形式如下:
priority_queue< type, container, function >
其中:
type:数据类型;
container:实现优先队列的底层容器;
function:元素之间的比较方式;
对于container,要求必须是数组形式实现的容器,例如vector、deque,而不能使list。在STL中,默认情况下(不加后面两个参数)是以vector为容器,以 operator< 为比较方式,所以在只使用第一个参数时,优先队列默认是一个最大堆,每次输出的堆顶元素是此时堆中的最大元素。
//构造一个空的优先队列(此优先队列默认为大顶堆)
priority_queue<int> big_heap;
//另一种构建大顶堆的方法
priority_queue<int,vector<int>,less<int> > big_heap2;
//构造一个空的优先队列,此优先队列是一个小顶堆
priority_queue<int,vector<int>,greater<int> > small_heap;
本次使用方法二来实现代码:
#include <iostream>
#include <queue>
using namespace std;
class MiddleFind {
private:
priority_queue<int> big_queue;
priority_queue<int, vector<int>, greater<int>> small_queue;
public:
MiddleFind() {
}
void AddItem(int val);
double GetMiddle();
};
void MiddleFind::AddItem(int val) {
//如果大顶堆为空则在大顶堆中插入元素
if (big_queue.size() == 0) {
big_queue.push(val);
return;
}
//如果大顶堆与小顶堆元素个数相同
if (big_queue.size() == small_queue.size()) {
if (val < big_queue.top()) {
//如果待插入值比大顶堆堆顶元素小则插入大顶堆并调整
big_queue.push(val);
} else {
//如果待插入值比大顶堆堆顶元素大则插入小顶堆并调整
small_queue.push(val);
}
}
//如果大顶堆元素个数多
else if (big_queue.size() > small_queue.size()) {
if (val > big_queue.top()) {
//如果待插入值比大顶堆堆顶元素大则插入小顶堆并调整
small_queue.push(val);
} else {
//如果待插入值比大顶堆堆顶元素小则将大顶堆顶元素插入小顶堆,将元素插入大顶堆
small_queue.push(big_queue.top());
big_queue.pop();
big_queue.push(val);
}
}
//如果小顶堆元素个数多
else if (big_queue.size() < small_queue.size()) {
if (val < small_queue.top()) {
//如果待插入值比小顶堆堆顶元素小,则插入大顶堆
big_queue.push(val);
} else {
//如果待插入值比小顶堆元素大,则把小顶堆堆顶元素移入大顶堆,将待插入元素插入小顶堆
big_queue.push(small_queue.top());
small_queue.pop();
small_queue.push(val);
}
}
}
double MiddleFind::GetMiddle() {
if (big_queue.size() == small_queue.size()){
return (big_queue.top() + small_queue.top()) / 2.0;
}
else if (big_queue.size() > small_queue.size()){
return big_queue.top();
}
else {
return small_queue.top();
}
}
int main(){
MiddleFind mf;
int a[10] = {1, 3, 5, 7, 9, 19, 17, 11, 15, 13};
for (int i = 0; i < 10; ++i) {
mf.AddItem(a[i]);
}
cout << "GetMiddle:" << mf.GetMiddle() << endl;
}
海量数据求TOP数
问题:10亿个搜索日志文件,如何快速获得TOP10热门关键词?场景限定单机,内存1GB。
方法:
1、将10亿条搜索关键词通过哈希算法分片到10个文件中
2、针对每个文件,利用散列表和堆,分别求出TOP10
3、把10个TOP10放一起,取TOP10
问题:求点击量排名TOP10,一个访问量非常大的新闻网站,希望将点击量排名 Top 10 的新闻摘要滚动显示在网站首页 banner 上,每隔 1 小时更新一次。
方法:
1、对每篇新闻摘要计算一个hashcode,使用map存储
2、每小时一个文件方式记录被点击的摘要
3、1小时结束后,计算最新一小时的点击TOP10,将摘要的hashcode分片到多个文件中,针对每个文件使用小顶堆统计TOP10
4、合并所有分片的TOP10,小顶堆,计算TOP10