注意:heap并不是STL的容器或者配接器,但却是priority queue的底层实现。并且heap运用的是常用的最大堆以及堆排序的方法,所以值得一看
一、最大堆和隐式表示法
- heap的数据结构是一个最大堆,最大堆就是一个完全二叉树,并且每个父节点都大于它的子节点(左右子节点的大小没有限制,不需要大的子节点一定要放哪边)
- 隐式表示法就是一个二叉树,可以用一个数组来表示,如图所示
任意一个节点的父子节点可以计算得到:
设某个节点索引值为index,则节点的左子节点索引为: 2*index+1
右子节点索引为: 2*index+2
父节点索引为: (index-1)/2
heap中总共有四个函数,push_heap,pop_heap,sort_heap,make_heap,下面依次介绍
二、push_heap
作用:将数组中最后一个数据插入到最大堆中
代码示例
int a[] = { 68, 31, 65, 21, 24, 32, 26, 19, 16, 13 };
vector<int> b(a, a + 10);//创建一个最大堆,并放入数组中
b.push_back(50);//把新加入的元素放在数组末尾
push_heap(b.begin(), b.end());//调用push_heap将数组末尾的元素放到合适的位置
for (int i = 0; i < (int)b.size(); i++)
cout << b[i] << " ";
结果如下:
过程图解
将50依次和自己的父节点比较,如果比父节点大,就和父节点交换位置,如果比父节点小,就结束这个过程。push_heap就是执行这个过程。
源码解析:
template <class _RandomAccessIterator, class _Distance, class _Tp>
void
__push_heap(_RandomAccessIterator __first,
_Distance __holeIndex, _Distance __topIndex, _Tp __value)
//这是真正算法执行的地方了,__holeIndex表示需要插入的值的当前位置,value就是需要插入最大堆的值了
{
_Distance __parent = (__holeIndex - 1) / 2;//找到父节点
while (__holeIndex > __topIndex && *(__first + __parent) < __value) {
//如果没有到堆顶或者需要插入的值大于父节点值的时候,循环都不会停止
*(__first + __holeIndex) = *(__first + __parent);//因为执行到这一步,肯定是value>*(__first + __parent),所以就把父节点的值下移到子节点
__holeIndex = __parent;//需要插入的值此时移到原父节点处
__parent = (__holeIndex - 1) / 2;//找到新位置的父节点
}
*(__first + __holeIndex) = __value;//将新值填入到最后的找到的合适位置,相当于__value是新值的临时存放位置,__holeIndex是新值索引的临时存放位置
}
template <class _RandomAccessIterator, class _Distance, class _Tp>
inline void
__push_heap_aux(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Distance*, _Tp*)
{
__push_heap(__first, _Distance((__last - __first) - 1), _Distance(0),
_Tp(*(__last - 1)));
//一般_Distance都是ptrdiff_t,这里就是把(__last - __first) - 1强转成ptrdiff_t类型。
//(__last - __first) - 1表示的是最后一个元素到堆顶的距离,也就是数组中最后一个元素的下标了
//_Distance(0)就是堆顶的下标了
}
template <class _RandomAccessIterator>
inline void
push_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
//传入首尾迭代器,会认为__first~__last-2都是已经排好的最大堆,而__last-1就是等待插入的元素
{
__push_heap_aux(__first, __last,
__DISTANCE_TYPE(__first), __VALUE_TYPE(__first));
}
三、pop_heap
作用:将最大堆中的根节点移出最大堆,移到数组的最后,所以相当于数组的尺寸没变,但是最大堆尺寸减一
代码示例
int a[] = { 68, 31, 65, 21, 24, 32, 26, 19, 16, 13 };
vector<int> b(a, a + 10);
b.push_back(50);
push_heap(b.begin(), b.end());
pop_heap(b.begin(), b.end());
for (int i = 0; i < (int)b.size(); i++)
cout << b[i] << " ";
结果如图所示:
过程图解
先将根节点放到数组最后一个位置,然后将该位置原元素,也就是24先临时保存起来
然后从根节点开始,比较它的子节点和24这三个数值,哪个更大,如果是其中一个子节点更大,那就把该子节点放到空着的根节点位置,这时就空出来一个子节点的位置(如步骤三中所示),就将24继续和空出来的节点的子节点比较,如果还是子节点更大,继续执行之前的流程,如果是24更大,就将24填入空着的节点。
源码解析:
template <class _RandomAccessIterator, class _Distance, class _Tp>
void
__adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex,
_Distance __len, _Tp __value)
//__holeIndex索引所在位置表示为空缺值,先让子节点不断上提弥补空缺值,然后将value加入到重新排列过的最大堆中。
{
_Distance __topIndex = __holeIndex;//__topIndex是根节点的索引
_Distance __secondChild = 2 * __holeIndex + 2;//找到__holeIndex的右子节点
while (__secondChild < __len) {//直到从根节点循环到最后一层才结束
//找到左右子节点中较大的那个值,将其索引值存入__secondChild
if (*(__first + __secondChild) < *(__first + (__secondChild - 1)))
__secondChild--;
//将__holeIndex的左右子节点中较大值填入__holeIndex位置
*(__first + __holeIndex) = *(__first + __secondChild);
__holeIndex = __secondChild;//__holeIndex向下移动到空着的子节点位置
__secondChild = 2 * (__secondChild + 1);//重新寻找右子节点
}
if (__secondChild == __len) {//这种情况下是右子节点不在堆的范围中,但是左子节点在,所以将左子节点上提
*(__first + __holeIndex) = *(__first + (__secondChild - 1));
__holeIndex = __secondChild - 1;
}
//将__value重新加入到最大堆中去
//并且此时初始位置是__holeIndex,这个位置的元素刚好被提到父节点去了
__push_heap(__first, __holeIndex, __topIndex, __value);
}
template <class _RandomAccessIterator, class _Tp, class _Distance>
inline void
__pop_heap(_RandomAccessIterator __first, _RandomAccessIterator __last,
_RandomAccessIterator __result, _Tp __value, _Distance*)
//__result是指向数组中的最后一个元素,用来存储根节点
//__value是原数组最后一个元素的值
{
*__result = *__first;
__adjust_heap(__first, _Distance(0), _Distance(__last - __first), __value);
//由于第三个参数是len,最大堆的尺寸,所以不需要-1,如果是最后一个元素的索引,就需要-1了
}
template <class _RandomAccessIterator, class _Tp>
inline void
__pop_heap_aux(_RandomAccessIterator __first, _RandomAccessIterator __last,
_Tp*)
{
__pop_heap(__first, __last - 1, __last - 1,
_Tp(*(__last - 1)), __DISTANCE_TYPE(__first));
//注意这边传入的参数把最大堆尺寸由原来的__last - __first - 1变为了__last - __first - 2
//相当于缩小了最大堆的尺寸
}
template <class _RandomAccessIterator>
inline void pop_heap(_RandomAccessIterator __first,
_RandomAccessIterator __last)//传入首尾迭代器
//将__first指向的元素放到__last-1,并重新调整最大堆的位置。
{
__pop_heap_aux(__first, __last, __VALUE_TYPE(__first));
}
四、sort_heap
作用:因为最大堆是存储在数组中的,所以利用最大堆把数组中的元素排序
代码示例:
int a[] = { 68, 31, 65, 21, 24, 32, 26, 19, 16, 13 };
vector<int> b(a, a + 10);
b.push_back(50);
push_heap(b.begin(), b.end());
sort_heap(b.begin(), b.end());
for (int i = 0; i < (int)b.size(); i++)
cout << b[i] << " ";
结果如图所示:
过程图解
就是不断使用pop_heap,每次都把堆中最大的值放到堆得末尾,然后找到剩下元素中最大的值放到根节点
源码解析
void sort_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
while (__last - __first > 1)//每执行一次,last都会减一,最大堆的规模都会减小一,所以知道最大堆上只有一个元素时,说明已经排序完毕。
pop_heap(__first, __last--);
}
五、make_heap
作用:就是将一个无序的数组排列成最大堆的形式
代码示例:
int a[] = { 24, 32, 31, 65, 26, 68, 19, 16, 21, 13 };//这里是无序的
vector<int> b(a, a + 10);
make_heap(b.begin(), b.end());
for (int i = 0; i < (int)b.size(); i++)
cout << b[i] << " ";
结果如图所示:
可以看到经过make_heap,就排列成最大堆的模式了。
源码解析
template <class _RandomAccessIterator, class _Tp, class _Distance>
void
__make_heap(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Tp*, _Distance*)
{
if (__last - __first < 2) return;//如果元素个数为1或者0就可以
_Distance __len = __last - __first;
_Distance __parent = (__len - 2)/2;//计算出最后一个元素__len - 2的父节点,然后从__parent 节点往前,就都是由子节点的了。
while (true) {
__adjust_heap(__first, __parent, __len, _Tp(*(__first + __parent)));//从最后一个父节点开始,每次把父
//节点下沉,相当于保证了每个父节点都比自己最相邻的那两个子节点大,然后依次往顶层循环,这样保证所有的父节
//点都比自己的子节点大了。
if (__parent == 0) return;
__parent--;
}
}
template <class _RandomAccessIterator>
inline void
make_heap(_RandomAccessIterator __first, _RandomAccessIterator __last)
{
__make_heap(__first, __last,
__VALUE_TYPE(__first), __DISTANCE_TYPE(__first));
}