C++ STL(第十篇:heap)

版权声明:转载请说明来源 https://blog.csdn.net/weixin_39640298/article/details/89194875

1、heap的概述

heap(堆)并不归属于 STL 容器组件,它是个幕后英雄,扮演 priority queue(优先队列)的助手。priority queue 允许用户以任何次序将任何元素推入容器,但取出时一定是从优先权最高的元素开始取。而 binary max heap(最大二叉堆)具有这样的特性,适合作为 priority queue 的底层机制。 因为后面要整理 priority queue,所以要先把 heap整理好。

虽然 binary search tree(二叉搜索树)也可以作为 priority queue 的底层。但杀鸡用牛刀,一来 binary search tree 的输入需要足够的随机性,二来 binary search tree 并不容易实现。 priority queue 的复杂度,最好介于 queue 和 binary search tree 之间,才适得其所。

所谓的 binary heap (二叉堆)就是一种 complete binary tree(完全二叉树),整棵 binary tree 除了最底层的叶节点之外是填满的,而最底层的叶节点由左至右又不得有空隙。如下图所示:
在这里插入图片描述
complete binary tree 整棵树没有任何节点漏洞,这带来一个极大的好处:我们可以利用array来存储所有节点。将 array 的 #0 元素保留(或设为无限大或无限小值),那么当 complete binary tree 中的某个节点位于 array 的 i 处时,其左子节点必位于array的 2i 处,其右子节点必位于 array 的 2i+1 处,其父节点必位于 “i/2” 处。通过这么简单的位置规则,array 可以轻易实现出 complete binary tree 。这种 array 表述 tree 的方式,我们称为隐式表述法

这样一来,我们需要的工具就很简单了:一个array和一组heap算法(用来插入元素、删除元素、取极值,将某一整组数据排列成一个heap)。array 的缺点是无法动态改变大小,而 heap 却需要这项功能,因此,以 vector 代替 array 是更好的选择。

根据元素排列方式,heap 可分为 max-heap(最大堆)和 min-heap(最小堆)两种,前者每个节点的键值(key)都大于或等于其子节点键值,后者的每个节点键值都小于或等于其子节点键值。因此,max-heap 的最大值在根节点,并总是位于底层 vector 的起头处;min-heap 的最小值在根节点,亦总是位于底层 vector 的起头处。STL 供应的是 max-heap,下面主要说的是 max-heap。

2、heap算法

堆算法才是 heap 的关键所在,以下介绍的都是STL提供的 堆泛型算法,随时可以拿来使用。如果把 max-heap的算法中的 > 改为 <= 则这个 max-heap 变为 min-heap。

2.1、push_heap算法

为了满足 complete binary tree 的条件,新加入的元素一定要放在最下一层作为叶节点,并填补在由左至右的第一个空格,也就是把新元素插入在底层 vector 的 end() 处

但是新元素是否合适于现在位置呢?为满足 max-heap 的条件(每个节点的键值都大于或等于其子节点键值),我们执行一个所谓的 percolate up(上溯)程序:将新节点拿来与父节点比较,如果其键值比父节点大, 就父子兑换位置。如此一直上溯,直到不需要对换或直到根节点为止。如下图所示:

在这里插入图片描述
其主要代码如下:

template<class RandomAccessIterator, class Distanch, class T>
void __push_heap(RandomAccessIterator first, Distance holeIndex, Distance topIndex,T value)
{
	//四个参数分别为:随机访问迭代器,插入数据的位置(尾端),其实位置的编号(一般为0),插入的值
	
	//第一步:找出新插入值的父节点
	Distance parent = (holeIndex  - 1 ) / 2;
	
	//第二步:判断是否尚未到达顶端,且父节点小于新值,如果是执行第三步,如果不是执行第五步
	while( holeIndex > topIndex && *(first + parent) < value)
	{
		//第三步:令当前节点的值等于父节点
		*( first + holeIndex) = * (first + parent);
		
		//第四步:更换当前节点的位置,并重新计算当前节点的父节点,然后跳到第二步判断
		holeIndex = parent;
		parent = (holeIndex -1) / 2;
	}
	
	//第五步:令满足条件的位置等于新插入的值
	*(first+holeIndex) = value;
}

2.2、pop_heap算法

pop操作是取走根节点,为了满足 complete binary tree 的条件,需要对 heap 的结构进行调整

因为是完全二叉树,取出元素之后还要保持完全二叉树,所以只能取走数组的最后一个元素,这里用到了所谓的 percolate down(下溯)的程序:将队尾节点和其较大子节点 “对调”,放置到队首的元素不符合 max-heap 的规则,则跟左右子树的值进行比较,然后下放,直到满足其要求。最后把数组的最后一个元素弹出就行了。如下图所示:

在这里插入图片描述

pop_heap的主要算法如下:

template<class RandomAccessIterator, class DIstance, class T>
void __adjust_heap(RandomAccessIterator first, Distance holeIndex, Distance len, T value)
{
	Distance topIndex = holeIndex;				//当前节点
	Distance secondChild = 2 * holeIndex + 2;	//当前节点的右子节点
	while( secondChild < len )
	{
		//比较左右两个子值,然后以SecondChild代表较大子节点
		if( *(first + secondChild ) < *(first + (secondChild - 1)))
			secondChild--;

		//令较大子值为当前值,令当前值移动到较大子节点处
		*(first + holeIndex) = *(first + secondChild);
		holeIndex = secondChild;
		
		//找出新当前值的右子节点
		secondChild = 2 * (secondChild + 1);
	}

	//没有右子节点,只有左子节点
	if( secondChild == len)
	{
		*( first + holeIndex) = *(first + secondChild - 1));
		holeIndex = secondChild - 1;
	}
	
	//虽然把队尾的元素,成功的下溯到叶节点,但是我们下溯的过程中没有对值进行验证,
	//所以队尾元素可能不满足 max-heap,需要在进行上溯
	__push_heap(first, holeIndex, topIndex, value);
};

下图是需要重新上溯的情况:
在这里插入图片描述

注意:pop_heap之后,最大元素只是被放置余底部容器最尾端,尚未被取走。如果要取走其值,可使用底部容器提供的back() 操作函数。如果要移除它,可使用底部容器所提供的 pop_back() 操作函数

3、sort_heap算法

sort_heap就是对堆进行排序。既然每次 pop_heap 可获得 heap 中键值最大的元素,如果持续对整个 heap 做 pop_heap 操作,每次将操作范围从后向前缩减一个元素(因为 pop_heap 会把键值最大的元素放在容器的最尾端),当整个程序执行完毕时,我们便有了一个递增序列。如下图所示:
在这里插入图片描述

template<class RandomAccessIterator>
void sort_heap(RandomAccessIterator first, RandomAccessIterator last)
{
	while( last - first > 1)
		pop_heap(first,last--);
}

注意:排序过后,原来的 heap 就不再是一个合法的 heap 了

4、make_heap算法

这个算法用来将一段现有的数据转化为一个 heap。通过不断调整子树,使得子树满足堆的特性来使得整个树满足堆的性质第一个需要执行调整的子树的根节点是从后往前的第一个非叶节点。从此节点往前到根节点对每个子树执行调整操作,即可构建堆

使用了前面介绍的方法,代码如下:

template<class RandomAccessIterator, class T, class Distance>
void __mak_heap(RandomAccessIterator first, RandomAccessIterator last, T*, Distance*)
{
	//如果长度为 0 或 1,不必重新排列
	if( last - first < 2) 
		return; 
	
	Distance len = last - first;
	//找出第一个需要重排的子树头部,以 holeIndex 标示出
	//因为任何叶节点都不需要执行下溯过程,所以需要使用头部节点
	Distance holeIndex = (len - 2)/2;
	
	//不断的重排子树,所有子树满足条件就退出
	while( true )
	{
		//重排以 holeIndex 为首的子树。len是为了判断操作范围
		__adjust_heap(first, holeIndex, len, T(*(first + holeIndex )));
		
		//走完根节点就结束
		if( holeIndex == 0) 
			return;
			
		holeIndex--;
	}
}

5、其它

heap 的所有元素都必须循序特别的排列规则,所以 heap 不提供遍历的功能,也不提供迭代器

感谢大家,我是假装很努力的YoungYangD(小羊)

参考资料:
《STL源码剖析》

猜你喜欢

转载自blog.csdn.net/weixin_39640298/article/details/89194875