堆是一种数据结构,本质上是数组,可以被视为一个完全二叉树结构。其又可以分为大堆和小堆。
大堆:每个父节点都大于孩子节点。
小堆:每个父节点都小于孩子节点。
以下由C++构成一个堆。
#include<vector>
#include<assert.h>
#include<iostream>
using namespace std;
template<class T>
struct Less
{
bool operator()(const T& l,const T& r)
{
return l<r;
}
};
template<class T>
struct Greater
{
bool operator()(const T& l,const T& r)
{
return l>r;
}
};
template<class T,class compare = Less<T>>
class Heap
{
public:
Heap()
{
}
Heap(T* a,size_t n)
{
_a.reserve(n);
for(size_t i = 0; i < n; i++)
{
_a.push_back(a[i]);
}
for(int i = (_a.size()-2)/2; i >= 0; i--)
//构建堆需要从最后一个叶节点的父节点开始
{
_AdjustDown(i);
}
}
void Push(const T& x)
{
_a.push_back(x);
_AdjustUp(_a.size()-1);
}
void Pop()
{
assert(_a.size() > 0);
swap(_a[0],_a[_a.size()-1]);
_a.pop_back();
_AdjustDown(0);
}
const T& Top()
{
assert(!_a.empty());
return _a[0];
}
size_t Size()
{
return _a.size();
}
bool Empty()
{
return _a.empty();
}
protected:
void _AdjustDown(int root) //向下调整
{
compare com;
int parent = root;
int child = parent*2+1;
while(child < _a.size()) // 孩子的下调范围不能超过数组大小
{
//if(child+1 < _a.size() && _a[child+1] > _a[child])
if(child+1 < _a.size() && com(_a[child+1] , _a[child]))
{
child++; //取子节点中最大的
}
//if(_a[parent] < _a[child])
if(com( _a[child],_a[parent]))
{
swap(_a[parent],_a[child]);
parent = child;
child = parent*2+1;
}
else
{
break;
}
}
}
void _AdjustUp(int child)
{
compare com;
int parent = (child-1)/2;
while(child > 0)
{
//if(_a[child] > _a[parent])
if(com(_a[child] , _a[parent]))
{
swap(_a[child],_a[parent]);
child = parent;
parent = (child-1)/2;
}
else
{
break;
}
}
}
private:
vector<T> _a;
};
其中的向上调整算法和向下调整算法。是关键。同时为了加强程序的复用功能,加入了仿函数。
所谓的向上调整,就是从最后一个叶节点的父节点开始,进行向上调整,当父节点小于孩子节点里最大的孩子节点时,将父节点和孩子节点互换,然后减到下一个节点,一次重复此过程。时间复杂度就是lgk.当进行最后一次的交换时,前提是左右子树已经成为了大堆。
向下调整,从最后一个节点和其父节点比较,如果大于其父节点,就交换,条件是孩子的位置不能小于数组的大小。
到了这里换必须再说一个问题,优先级队列。我们可以用对来构造一个。
template<class T, class Comapre>
class PriorityQueue
{
public:
void Push(const T& x)
{
_hp.Push(x);
}
void Pop()
{
_hp.Pop();
}
const T& Top()
{
return _hp.Top();
}
protected:
Heap<T, Comapre> _hp;
};
接下来就是不能不说的排序问题了。
对于堆排序,升序建大堆,先把排序的数建堆,此时堆头的数时最大的,将其与最后一个互换,在进行自顶向下的调整(此时左右子树都是大堆),调整后堆头的数又是最大的,在进行一次刚才的操作,直至处理完。
降序建小堆(方法同升序)。
void Adjustdown(int *a,int n,int root)
{
int parent = root;
int child = parent*2+1;
while(child < n)
{
if(child+1 < n && a[child] > a[child+1])
{
child++;
}
if(a[child] < a[parent])
{
swap(a[child],a[parent]);
parent = child;
child = parent*2+1;
}
else
{
break;
}
}
}
void heapsort(int* a,size_t n)
{
//降序 构建小堆
for(int i = (n-2)/2;i >= 0; i--)
{
Adjustdown(a,n,i);
}
int end = n-1;
while(end > 0)
{
swap(a[0],a[end]);
Adjustdown(a,end,0);
end--;
}
}
void TestHeapSort()
{
int a[] = {2,3,6,8,9,3,5,6,10};
heapsort(a, sizeof(a)/sizeof(a[0]));
}
堆的另一个应用就是解决海量数据处理。
例如:
(1)1 0 0 w个数中找出最大的前K个数
(2) 2 0 1 5 年春节期间,A公司的支付软件某宝和T公司某信红包大乱战。春节后高峰以后,公司L eader 要求后台的攻城狮对后台的海量
数据进行分析。先要求分析出各地区发红包金额最多的前1 0 0 用户。现在知道人数最多的s 地区大约有1 0 0 0 w用户。要求写一个算法
实现。【扩展:海量数据处理】
就第一个简单说一下。找出最大的K个数,在100W个数里,采用很高的效率,无疑一个好的方法就是建堆。先建立一个k个数的小堆,堆顶的元素最小,然后将K~100W的元素逐一比较,如果大于堆顶的元素就和堆顶的元素交换,然后在进行一次向下调整。话不多少,线上代码,帮你理解我的问题。
void Adjustdown(vector<int>& heap,int root)
{ //建立小堆
int parent = root;
int child = parent*2+1;
while(child < heap.size())
{
if(child+1 < heap.size() && heap[child] > heap[child+1])
{
child++;
}
if(heap[child] < heap[parent])
{
swap(heap[child],heap[parent]);
parent = child;
child = parent*2+1;
}
else
{
break;
}
}
}
void TopK( vector<int>& a,int k)
{
assert(a.size() > k);
vector<int> topk;
topk.reserve(k);
for(int i = 0; i < k; i++)
{
topk.push_back(a[i]);
}
for(int i = (k-2)/2; i >= 0; i--)
{
Adjustdown(topk,i);
}
for(int i = k; i < a.size();i++)
{
if(topk[0] < a[i])
{
topk[0] = a[i];
Adjustdown(topk,0);
}
}
for(int i = 0; i < k; i++)
{
cout<<topk[i]<<" ";
}
cout<<endl;
}
void testBig()
{
vector<int> a;
a.resize(10000,10);
a[99] = 99;
a[199] = 199;
a[299] = 299;
a[399] = 399;
a[4] = 100;
a[5] = 101;
a[6] = 102;
a[7] = 103;
a[40] = 104;
a[55] = 105;
a[60] = 106;
a[70] = 107;
TopK(a,20);
}