堆是一个用途很广泛的数据结构,是实现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;
}