堆是一个用途很广泛的数据结构,是实现topK问题、堆排序以及优先级队列等问题的必备工具。深刻理解堆这种数据结构,掌握实现堆的技能是学习数据结构非常重要和必备的
一环。
堆的本质是:
vector + 向上调整 和 向下调整
人们在想象中把它抽象为一棵每个父亲节点都比它两个子节点 大/小 的二叉树
并用二叉树的下标表示方法构建和调整这棵树。实际上所有的结点都在vector中
顺序存储。
向下调整:
(_v.size() - 2) >> 2 找到最后一个父亲结点 若其比儿子中的某一个小 则将两个孩子中较大/小的结点与其父亲结点互换 之后 parent = child ; child= parent*2
+1;(向下继续处理)Pop() 和 构造堆要用到。
向上调整:
Push()时要用到 这时原树已经是堆了 在其最后加一个结点然后 查看新加结点是否比父亲大/小 如果是交换它和其父亲 之后 child = parent; parent= (child - 1)>>1.
以下是实现一个堆的代码:
“堆.cpp”
#pragma
once
#include<iostream>
#include<string.h>
#include<vector>
#include<assert.h>
using
namespace
std;
//利用仿函数代替比较运算符
//Compare()(_v[child], _v[]parent) 其实是Compare( ) 创建一个临时对象 调用其operator()
template<class
T>
struct
Greater
{
bool
operator ()(const
T& l,
const
T& r)
{
return
l > r;
}
};
template<class
T>
struct
Less
{
bool
operator ()(const
T& l,
const
T& r)
{
return
l < r;
}
};
template<class
T,
class
Compare
=
Greater<T>>
class
Heap
{
public:
Heap()
{}
bool
Empty()
{
return
_v.empty();
}
size_t
Size()
{
return
_v.size();
}
Heap(const
T* arr,int
n)
{
_v.reserve(n);//为了提高效率
一次性申请够空间 防止插入时空间不够再次申请。
for
(int
i = 0; i < n; ++i)
{
_v.push_back(arr[i]);//一般和reserve()函数配合使用
}
for
(int
i = (int)(_v.size()
- 2) / 2; i >= 0; --i)
{
AdjustDown(i);//请注意向下调整是被循环调用的
最早从树的最后一个父亲结点开始
//目的是为了将原本vector中无规律的数变为符合堆的规律。
//每一次调用完AdjustDown(i)只保证 i下子树是堆
}
}
void
Pop()//移除堆顶方法
{
swap(_v[_v.size()
- 1], _v[0]);//先将堆顶结点和堆最后结点互换位置
_v.pop_back();//之后移除原本是对顶的最后一个结点
//之所以这么做是为了让原对顶的左右子树保持堆的性质。
AdjustDown(0);//把原来是最后结点的现堆顶搞一次向下调整。
}
void
Push(const
T& data)//新插入结点
{
_v.push_back(data);//先将新结点权且放在最后以保证堆内其他结点符合堆的性质
AdjustUp(_v.size()
- 1);//注意调用向上调整时只调一次 向上调整是为了Push而存在的
}
const
T&
Top()
{
return
_v[0];
}
void
Printheap()
{
for
(size_t
i = 0; i < _v.size(); ++i){
cout << _v[i] <<
" ";
}
cout <<
endl;
}
private:
void
AdjustDown(int
root)//向下调整
{
int
parent = root;//已有最后一个父亲结点
int
child = parent * 2 + 1;//默认找到其左孩子
while
(child < _v.size()){
if
(child + 1 < _v.size() &&
Compare()(_v[child + 1], _v[child]))
{
child++;//这里是为了找到两个孩子里较大的准备和父亲换
}
else
if
(Compare()(_v[child], _v[parent])){
swap(_v[child],
_v[parent]);//如果大孩子比父亲大 则换
parent = child;
child = parent * 2 + 1;//向下继续
}
else{
break;//大孩子比父亲小
次树是堆出循环
}
}
}
void
AdjustUp(int
index)//向上调整
{//注意此时除index外其余结点符合堆的性质
int
child = index;
int
parent = (child - 1) / 2;
while
(child > 0){
if
(Compare()(_v[child], _v[parent])){
swap(_v[child],
_v[parent]);//比父亲大和父亲换
child = parent;
parent = (child - 1) / 2;//向上调整
}
else{
break;//比父亲小了
出循环
}
}
}
vector<int>
_v;
};
topk问题(也是海量数据处理问题):
问题:需要从十亿个数据中找出最大的前k个数。
分析思路:
思路:不能使用排序,因为使用排序的话,内存就必须可以容纳十亿个数据,但是这很明显不可能,所以不能使用排序。
我们可以先从十亿个数据中取出前k(假如为100)个数据,使用这k个数据来建一个小堆,那么堆顶就是这个堆中最小的数据,
然后我们就从十亿减k个数据中取出一个数据赖和堆顶数据来进行比较,如果比堆大,那么就拿这个数据来替换堆顶数据。
然后采用向下调整算法,使之堆顶又是这个堆中最小的数据。依次比较。。替换。。。调整。。。
最终,堆中的前k个数据就是十亿个数据中最大的k个数据。
这里应该可以想到:最大的前k个数据一定会进堆。–>因为每次都是比较堆顶的数据和从剩余的数据进行比较,
而这个堆顶数据又是这个堆中最小的数据。
注意:不能建大堆,如果建大堆,然后从剩余的十亿数据中取数据,拿取到的数据和堆顶数据进行比较,
如果这个数据大于堆顶的数据,那么就交换两者的数据。最终只能找出十亿数据中最大的一个数据。—->不符合。
以下是关于对的优先级队列的使用 及topK问题的解决:
#include"堆.cpp"
#define
N
1000
#define
K
20
//优先级队列为堆的简单封装
template<class
T>
class
PriorityQueue
{
public:
PriorityQueue()
{}
void
Push(const
T& data)
{
hp.Push(data);
}
void
Pop()
{
hp.Pop();
}
const
T&
Top()
{
return
hp.Top();
}
size_t
Size()
{
return
hp.Size();
}
bool
Empty()
{
return
hp.Empty();
}
private:
Heap<int,
Greater<int>>
hp;
};
void
Test()
{
int
array[] = { 53, 17, 78, 9, 45, 65, 87, 23 };
Heap<int,
Greater<int>
> h(array,
sizeof(array) /
sizeof(int));
h.Push(80);
h.Pop();
}
void
TestPriorityQueue()//测试优先级队列
{
int
array[] = { 53, 17, 78, 9, 45, 65, 87, 23 };
size_t
len =
sizeof(array) /
sizeof(int);
PriorityQueue<int>
p;
for
(size_t
index = 0; index < len; ++index)
{
p.Push(array[index]);
}
cout << p.Top()
<<
endl;
p.Pop();
cout << p.Top()
<<
endl;
}
//TopK问题
void
TopK()
{
int
arr[N];
int
brr[K];
for
(size_t
i = 0; i <
N; ++i){
arr[i] =
rand() %
N;
}
for
(size_t
i = 0; i <
K; ++i){
brr[i] = arr[i];
}
Heap<int,
Less<int>>
hp(brr,
sizeof(brr) /
sizeof(int));
for
(size_t
i = K; i <=
N; ++i){
if
(arr[i] > hp.Top()){
hp.Pop();
hp.Push(arr[i]);
}
}
hp.Printheap();
}
int
main()
{
/*Test();
TestQueue();*/
TopK();
system("pause");
return
0;
}