1、基本概念
在stl中,heap并不是做成容器,而是作为算法来使用的。介绍heap之前,我们先要说明一下完全二叉树。
完全二叉树,首先是二叉树,其次除了最底层,其他层的节点个数都达到该层节点数的最大值。同时,最底层的节点是按照从左至右连续排列的。我们假设根节点的索引位置为1,则有以下特性:
1. 对于索引位置为n的节点,其到根节点的节点总数也为n;
2. 对于索引位置为n的节点,其左子女的索引位置为2*n,右子女的索引位置为2*n+1;
3. 对于索引位置为n(n>1)的节点,其父节点的索引位置为int(n/2)。
通过完全二叉树的定义,我们可以很好的利用array来存储完全二叉树。
heap就是一种完全二叉树,同时,有序heap也有自己的限制条件。heap分为max-heap和min-heap,前者要求树中的任意节点的键值要大于等于其子节点的键值,后者要求书中的任意节点的键值要小于等于其子节点的键值。因而,max-heap的根节点存储的是键值最大的节点,min-heap的根节点存储的是键值最小的节点。
这里要说明的是,stl在实现heap算法是,是把根节点的索引位置设为0,这样与array更加契合,但是要调整计算公式:
1. 节点的索引位置为其到根节点的节点个数n-1;
2. 对于索引位置为n的节点,其左子女的索引位置为2*n+1,其右子女的索引位置为2*n+1;
3. 对于索引位置为n(n>0)的节点,其父节点的索引位置为int((n-1)/2)。
2、构建堆算法make_heap
make_heap默认使用小于进行比较,因而构建的是max-heap。通过使用max-heap算法,可以是序列按照堆的方式进行顺序的调整,以下为测试代码:
int data2[9] = {0,1,2,3,4,8,9,3,5};
make_heap(data2,data2+9);
for (int i =0;i<9;++i)
{
cout<<data2[i]<<" "; //9 5 8 3 4 0 2 3 1
}
cout<<endl;
make_heap的算法步骤为:
1. 若节点个数少于两个则不必调整,直接返回;
2. 通过节点总数len可知最后一个节点的索引位置为len-1,根据公式得出该节点的父节点的索引位置parent =((len-1) - 1)/ 2;
3. 重排以parent节点为根节点的子树;
4. 若parent = 0 ,则结束;反之,将parent移向该节点的钱一个节点parent - 1,重复步骤3。
根据完全二叉树的特性,我们可以知道最后一个节点的父节点之后的节点必然为叶子节点,没有必要重排,直接跳过,其他节点都要进行一次重排。
重排子树的算法是独立开来的,去步骤为:
1. 根据算法的参数空洞节点holeIndex,得到其右子女secondChild=2*holeIndex+2;
2. 将右子女的索引位置与节点个数len进行比较,根据结果进行不同的处理;
3. 若小于len则未越界,则取左右子女中较大的节点,将holdIndex节点的键值设为该节点的值,并将holdIndex指向该节点,然后跳转到步骤1;
4. 若等于len则可知道最后一个节点为左子女secondChild-1,将holdIndex节点的键值设为该节点的值,并将holdIndex指向该节点,然后跳转到步骤1;
5. 若大于len则holdIndex已位于叶子节点,不需要再向下进行判断了。此时,使用插入算法处理holdIndex和要调整的值value。
3、插入算法push_heap
push_heap要求底层数据结构先把数据插入到序列的尾部,然后才能使用该算法。array由于是固定大小,并不能插入新的数据,这里我们使用vector作为数据的存储结构,以下是测试代码:
nt data[9] = {0,1,2,3,4,8,9,3,5};
vector<int> a(data, data+9);
for (int i =0;i<a.size();++i)
{
cout<<a[i]<<" "; //0 1 2 3 4 8 9 3 5
}
cout<<endl;
make_heap(a.begin(),a.end());
for (int i =0;i<a.size();++i)
{
cout<<a[i]<<" "; //9 5 8 3 4 0 2 3 1
}
cout<<endl;
a.push_back(7);
push_heap(a.begin(),a.end());
for (int i =0;i<a.size();++i)
{
cout<<a[i]<<" "; //9 7 8 3 5 0 2 3 1 4
}
cout<<endl;
通过log可以看到,必须先自行将数据插入到尾部,然后才可以调用push_heap算法。该算法的具体步骤为:
1. 设置空洞holeIndex为尾节点,并记录尾节点的值value;
2. 获取holeIndex的父节点parent = int((holeIndex -1) / 2);
3. 若holeIndex为不为顶端节点,且父节点的键值小于value,则将holeIndex的键值设为父节点的键值,并将holeIndex指向父节点,然后跳转到步骤2;
4. 此时holeIndex为其最终的位置,将其键值设为value。
4、移除算法pop_heap
pop_heap算法会将头节点的键值移到序列的尾部,并不会实际的删除节点,删除操作需要底层结构自行处理,一下为测试代码:
pop_heap(a.begin(),a.end());
for (int i =0;i<a.size();++i)
{
cout<<a[i]<<" "; //8 7 4 3 5 0 2 3 1 9,堆顶的值被移到最后,但不删除
}
cout<<endl;
a.pop_back();
for (int i =0;i<a.size();++i)
{
cout<<a[i]<<" "; //8 7 4 3 5 0 2 3 1
}
cout<<endl;
下面是pop_heap的算法步骤:
1. 记录尾节点的键值value,将尾节点的键值设为根节点的键值;
2. 标记空洞holeIndex为根节点,并使用子树重排的算法对[first,last-1)的子树进行重排。
这里看起来步骤很简单,只是使用了之前的算法来帮助完成。需要注意的是子树重排算法,这个在make_heap和pop_heap中都有使用到。
5、排序算法sort_heap
通过pop_heap我们可以看到,其可以将序列极值移到队尾,若是不断进行此操作,就可得到一个有序序列,从而达到排序的目的,以下是测试代码:
sort_heap(a.begin(),a.end());
for (int i =0;i<a.size();++i)
{
cout<<a[i]<<" "; //0 1 2 3 3 4 5 7 8
}
cout<<endl;
这里对最大堆排序后生成升序排序,同理对最小堆排序后生成的是降序排序。经过排序的heap不再是一个合法的heap,可以通过make_heap重新构建。
6、比较函数
stl的heap默认使用小于作为比较函数,因而构建的是max-heap,若要构建min-heap,需要在构造时设置比较函数,测试如下:
#include <xfunctional> //比较函数需要引用的头文件
int data3[9] = {8,1,2,3,4,0,9,3,5};
vector<int> x(data3,data3+9);
for (int i =0;i<x.size();++i)
{
cout<<x[i]<<" "; //8 1 2 3 4 0 9 3 5
}
cout<<endl;
make_heap(x.begin(),x.end(),greater<int>());
for (int i =0;i<x.size();++i)
{
cout<<x[i]<<" "; //0 1 2 3 4 8 9 3 5
}
cout<<endl;
这样就构建了一个min-heap,但是heap相关算法必须保持一致的比较函数,即不能在一个min-heap上使用max-heap的push_heap,这样会造成错误的结果,测试如下:
x.push_back(7);
push_heap(x.begin(),x.end());
for (int i =0;i<x.size();++i)
{
cout<<x[i]<<" "; //7 0 2 3 1 8 9 3 5 4,在最小堆上应用最大堆的插入方式,结果错乱
}
cout<<endl;
这里可以看到,push_heap按照max-heap的方式将7设置到了头节点,这样显然是不正确的,使用的时候一定要注意这点。
7、迭代器
heap本身的元素必须按照特定的规则进行操作,不提供元素的遍历功能,也不提供迭代器。