heap概述
严格意义来说,heap不属于stl容器组件,它是priority queue的助手。这里要讲解的heap是binary heap,其实就是一种完全二叉树。
我们以vector对tree进行表述,这种方法称为隐式表述法。
push_heap算法
template<class RandomAccessIterator>
inline void push_heap(RandomAccessIterator first, RandomAccessIterator last) {
__push_heap_aux(first, last, distance_type(first), value_type(first));
}
template<class RandomAccessIterator,class Distance,class T>
inline void __push_heap_aux(RandomAccessIterator first, RandomAccessIterator last, Distance*, T*) {
//迭代器是左开右闭原则,所以最后一个元素的地址是last-1
//也正是因为上述原因,vector首尾元素之间的距离为(last-first)-1
__push_heap(first, Distance((last - first) - 1), Distance(0), T(*(last - 1)));
}
新插入的元素置于底部容器的最尾端,因为这并不是最终的位置,所以先置为空洞。
template<class RandomAccessIterator,class Distance,class T>
void __push_heap(RandomAccessIterator first, Distance holeIndex, Distance topIndex, T value) {
/*
最开始很困惑为什么父节点是(holeIndex-1)/2,而不是holeIndex-1,
你要注意的是这不是指父节点,而是说父节点到first指向的起始元素
的距离,包括这里的holeIndex也不是说空洞的坐标,不是此时的空洞
与first指向的起始元素的距离,通过上面的那张图就能看出为什么要
减一了
*/
Distance parent = (holeIndex - 1) / 2;
/*
此处的topIndex是0,代表首元素到首元素的距离,first + parent
这才是真正的地址下标
*/
while (holeIndex > topIndex && *(first + parent) < value) {
/*
(first + parent) < value代表是父节点的值小于这个后代的值,
如果成立,就将父节点的值移置空洞,然后将父节点所在位置
设为空洞
*/
*(first + holeIndex) = *(first + parent);
holeIndex = parent;//这两个都是距离
parent = (holeIndex - 1) / 2;//新洞的父节点到起始元素的距离
}
//循环结束的时候新插入元素的位置即在空洞所处
*(first + holeIndex) = value;
}
pop_heap算法
以大顶堆为例,最大值必然在根结点,pop操作取走根结点(其实就是设至底部容器vector的尾端节点)后,割舍最下层右边的叶结点,先调整此时的堆,然后再将割舍掉的元素值插入。
template<class RandomAccessIterator>
inline void pop_heap(RandomAccessIterator first, RandomAccessIterator last) {
__pop_heap_aux(first, last, value_type(first));
}
template<class RandomAccessIterator,class T>
inline void __pop_heap_aux(RandomAccessIterator first, RandomAccessIterator last, T*) {
__pop_heap(first, last - 1, last - 1, T(*(last - 1)),distance_type(first));
}
template<class RandomAccessIterator,class T,class Distance>
inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last,
RandomAccessIterator result, T value, Distance*) {
*result = *first;//设定尾值为首值(最大元素),然后取出
//取走以后调整heap,空洞为0(0代表空洞距树根的距离),欲调整值为value(也就是原尾值)
__adjust_heap(first, Distance(0), Distance(last - fiirst), value);
}
template<class RandomAccessIterator,class Distance,class T>
void __adjust_heap(RandomAccessIterator first, Distance holeIndex,
Distance len, T value) {
//topIndex也就是据树根距离的最小值(0)
Distance topIndex = holeIndex;
//secondChild代表空洞节点的右孩子节点到树根的距离,要注意初始的holeIndex为2
Distance secondChild = 2 * holeIndex + 2;
//len代表至树根的最远距离
while (secondChild < len) {
/*
*(first + secondChild)是右孩子的位置
*(first + (secondChild - 1))是左孩子的位置
找到它俩中的较大者移至空洞所在位置
然后将较大者的原有位置设为空洞
*/
if (*(first + secondChild) < *(first + (secondChild - 1)))
secondChild--;
*(first + holeIndex) = *(first + secondChild);
holeIndex = secondChild;
/*
跟新空洞所在处的右孩子
因为secondChild代表的是距离,所以
不要想成secondChild=2*secondChild+1,看上图
*/
secondChild = 2 * (secondChild + 1);
}
//下面这步所示情况如下图所示
if (secondChild == len) {//没有又节点,只有左节点
//令左孩子值为空洞值,再将空洞移至左孩子处
*(first + holeIndex) = *(first + (secondChild - 1));
holeIndex = secondChild - 1;
}
__push_heap(first, holeIndex, topIndex, value);
}
这张图描述的特殊情况就是上面的if语句,将10移至0处,然后secondChild=2*(econdChild+1)=3;而len描述的是至树根的最远距离也为3,此时就只有左孩子而没有右孩子。(注意:上述的红色角标仍旧是代表距离树根的距离)
小结
pop_heap之后,最大元素只是被置放在底部容器的最尾端,尚未被取走,如果要取其值,可使用底部容器(vector)所提供的back()操作函数,如果要移除它,可使用底部容器(vector)所提供的pop_back()操作函数。
sort_heap算法
既然每次pop_heap可获得heap中键值最大的元素,若持续对整个heap做pop_heap操作,每次将操作方位从后向前缩减一个元素(因为pop_back会把键值最大的元素放在容器的最尾端),当整个程序执行完毕的时候,便形成了一个递增序列。
template<class RandomAccessIterator>
void sort_heap(RandomAccessIterator first, RandomAccessIterator last) {
while (last - first > 1)
//每次执行pop_heap()一次,操作范围退缩一格
pop_heap(first, last--);
}
make_heap算法
template<class RandomAccessIterator>
inline void make_heap(RandomAccessIterator first, RandomAccessIterator last) {
__male_heap(first, last, value_type(first), distance_type(first));
}
template<class RandomAccessIterator,class T,class Distance>
void __male_heap(RandomAccessIterator first, RandomAccessIterator last,
T*, Distance*) {
/*
小心,holeIndex是距离类型
holeIndex是需要重排的子树头部,从最后一个有子树的头部开始,利用
__adjust_heap进行调整,然后holeIndex向前,继续调整
*/
Distance holeIndex = (len - 2) / 2;
while (true) {
__adjust_heap(first, holeIndex, len, T(*(first + holeIndex)));
if (holeIndex == 0) return;
holeIndex--;
}
}
通过下图可以看出为什么第一个空洞是holeIndex=(len-2)/2;
迭代器first为0,last为10,len=last-first=10;
很明显可以看出,距离为5、6、7、8、9的节点均满足大顶堆的性质,因为它们没有左右孩子,所以需要重新构建的第一个空洞就是最后一个右孩子的节点,即为距离为(10-2)/2=4的节点,然后依次向前。
实现一个堆排序
#include <iostream>
#include<algorithm>
using namespace std;
//设计时第一个元素的索引是1
void HeapAdjust(int *a, int i, int size) //调整堆
{
int lchild = 2 * i; //i的左孩子节点序号
int rchild = 2 * i + 1; //i的右孩子节点序号
int max = i; //临时变量
if (i <= size / 2) //如果i不是叶节点就不用进行调整
{
if (lchild <= size&&a[lchild] > a[max])
{
max = lchild;
}
if (rchild <= size&&a[rchild] > a[max])
{
max = rchild;
}
if (max != i)
{
swap(a[i], a[max]);
HeapAdjust(a, max, size); //避免调整之后以max为父节点的子树不是堆
}
}
}
void BuildHeap(int *a, int size) //建立堆
{
int i;
for (i = size / 2; i >= 1; i--) //非叶节点最大序号值为size/2
{
HeapAdjust(a, i, size);
}
}
void HeapSort(int *a, int size) //堆排序
{
int i;
BuildHeap(a, size);
for (i = size; i >= 1; i--)
{
cout<<a[1]<<" ";
swap(a[1], a[i]); //交换堆顶和最后一个元素,即每次将剩余元素中的最大者放到最后面
//BuildHeap(a,i-1); 将余下元素重新建立为大顶堆
HeapAdjust(a, 1, i - 1); //重新调整堆顶节点成为大顶堆
}
}
int main(void)
{
int nums[] = { -1,3,8,5,2,4,9 };
HeapSort(nums, 6);
system("pause");
return 0;
}