算法学习-数据结构-堆模板

本文深入介绍了堆这种数据结构,包括最小堆和最大堆的定义及性质,详细阐述了堆的上移、下移操作,以及元素插入、删除的算法复杂度。此外,还讲解了如何使用C++实现堆模板,并探讨了在STL中如何使用堆进行数据排序。文章最后提及了堆在ACM竞赛中的应用问题。

堆是一种以数组形式存放数据的数据结构,并且也具有二叉树的某些性质的数据结构,因此可以使用堆可以很有效地方问和检索堆中的数据,很方便地对其进行插入和删除操作。

本文将会分成三个部分来介绍堆,

第一部分从堆的形式化的定义和堆中所定义的堆操作的接口定义。

第二部分给出堆接口定义中的方法实现,即算法复杂度的分析,最后实现一个以堆为类名的类模板的封装。

第三部分介绍在STL中如何使用堆来进行数据的排序

第四部分给出在ACM中使用堆来解决问题的问题和解题报告

poj 2010 & poj 2823 & poj2442 &poj3481&poj2051


第一部分:


1.堆的定义

n 个元素我们称之为堆,当且仅当它的关键字序列 k1 , k2 , k3 ..... , kn 满足:


    ;      ( 1.1 )


或者满足:

      ;   ;    (1.2)


我们把满足式子 1.1 的堆称之为最小堆, 而把满足式子 1.2 的堆称之为最大堆, 对于堆来说,可以从它的定义看出来,实质上它是一个完全二叉树。

如果树的高度为 d , 并约定根的层次为 0 , 则堆是具有如下性质的:

1,所有的叶节点不是出于第 d 层,就是出于 d-1 层。。

2,当 d >= 1 的时候, 在第 d-1 层上有 2^(d-1) 个节点 。

3, 第 d-1 层上如果有分支节点,则这些分支节点都集中在树的最左边。

4. 每个节点所存放元素的关键字,都大于(最大堆) 或 小于 (最小堆)其子孙节点所存放的关键字。


对于一个具有n个元素的堆,可以很方便地用如下的方法,有数组H来存取它。

1. 根节点通常存放在 H[1] 中。

2. 假定结点 x 存放在 H[i] , 如果它有左儿子,则左儿子节点存放在 H[2i] 中, 如果有右儿子节点,则右儿子节点存放在 H[2i+1] 中

3. 非根节点 H[i] 的父节点存放在    中。


堆操作的接口定义:


void sift_up ( Type H [] , int i ) ;
//作用是将堆中第 i 个元素上移 

void sift_down ( Type H [] , int n , int i );
//作用是将堆中的第 i 个元素下移

/**
在这里是这样的, 对于元素在堆中上移的时候,是从底到上进行不断比较,各个节点上面的元素的大小的,所以到根 H[1] 这个过程就会停止,
但是在元素堆中下移的时候,是需要通过堆中元素的大小从当前位置到 堆中最后一个元素进行比对的, 如果不指定最后一个元素的位置,或是堆中元素的多少的话,会造成越界访问的问题。
*/

void insert ( Type H [] , int &n , Type x ) ;
//该操作实现的是将元素 x  插入到堆中

Type delete ( Type H [] , int &n , int i ) ; 
//该操作实现的是将元素 x 从堆中删除,并且将删除的元素作为
//返回值进行返回

void make_head ( Type H [] , int n ) ;
// 使数组 H 中的元素按照堆结构从新的组织


好了, 第一部分结束,下面我们来看第二部分.


第二部分:


1. 元素上移操作

假定所使用的堆是最大堆。当修改了堆中某一个节点的关键字的数值的时候,使得这个关键字的数值大于它父亲的关键字,这就会打破最大堆中父节点大于两个子节点中的元素值的平衡,即违反了最大堆的性质。为了重新恢复最大堆的性质,需要把该元素上移到合适的位置。这个时候我们可以通过 sift_up 这个方法来使得整个堆再次恢复到平衡的状态。

其中,sift_up 操作会沿着 H[i] 到根 H[i] 的一条路线,把 H[i] 向上移动。 在移动的过程中, 把它和它的父节点进行比较,即比较 H[i] 和 H [i/2] 的大小,如果

H[i] > H[i/2] 的话, 则将置换 H[i] <-> H[i/2] 二者的位置。如此进行下去,直至H[i] 找到一个合适的位置为止。



/*
算法 1 元素的上移操作
输入: 数组 H [] 及被上移的元素在数组 H 中的下标 i
输出: 维持堆的性质的数组 H [] 
*/

template <class Type >
void sift_up ( Type H [] , int  i )
{
	bool done = false ;

	if ( i != 1 )
	{
		while ( !done && i != 1 )
		{
			if ( H [i] > H[i/2] )
				swap (H[i] , H[i/2]) ;
			else
				done = true ;
			i = i/2 ;
		}
	}
}

template < class Type >
void swap ( Type &a , Type &b )
{
	Type h = a ;
	a = b ;
	b = h ;
}


算法中的 i/2 操作是整出操作。在后面的算法中, 若是 a, b 是整数的话,则操作 a/b 均是表示的是整除操作。元素每进行一次移动,就会执行一次比较

操作。如果移动成功的话,他所在节点的层数就会-1. n 个元素一共有 (logn) 下取整,层的节点,所以sift_up 对应执行最多次数的操作是,i 的值是位于

堆的最下层,但是其 H[i] = Max ( H [k] )  1<=k<=n 。 所以 H[i] 的位置将会从 第logn 层,一直移动到 第 1 层。 由此可知,sift_up 操作的执行时间是 O(logn) , 同时我们还可以看到的就是算法运行期间,需要额外的空间为 1 个用来临时存放两个节点之间元素交换的空间,所以可知堆操作的元素上移操作需要的空间复杂度为 O(1) .


2. 元素下移操作

   有元素上移操作,就一定有与之对应的元素下移操作,它通常发生在与元素上移操作相同的情境下,即当修改最大堆中的某一个节点元素的关键字,使其小于其儿子节点的关键字的时候,也同样违反了最大堆的性质,即打破了局部对平衡的性质。为了重新使得整个堆回复最大堆的性质,需要把该元素下移动到合适的位置。这个时候,调用方法 sift_down 即可以实现平衡堆的目的。

  在sift_down 的操作中,方法中会不断的将 H [i] 与max ( H[i*2] , H[i*2+1] ) 进行比较, 如果小于其中较大的节点的话,那么就将节点中的关键字互换,然后继续向下比较,直至找到一个H[i] 合适的位置为止 。sift_down 的代码描述如下所示:


/**
算法 2 元素的下移操作
输入: 数组 H[] , 数组 H [] 中元素的个数,将会被向下移动的元素在数组中的下标

输出: 重新恢复堆平衡性质的堆,即数组H []
*/

template <class Type>
void sift_down ( Type H [] , int n , int i )
{
	bool done = false ;

	if ((2*i) < n ) //首要条件是,当前待比较的 i 节点的子节点没有达到堆中的最后一个元素
	{
		//循环条件是,如果找到合适的位置之后,done = true , 停止循环
		//如果当前待比较、交换位置的节点在堆中的位置越界,
		//即超出了堆中节点 n 的个数的限制,则停止比较置换操作, 停止循环
		while(!done && ((i=2*i)<=n))
		{
			/**
			在这里的算法运行情况是这样的,在这里并没有使用max(H[i*2] ,H[i*2+1] )
			而是采用,在循环操作中直接现将 i 的数值乘以 2 的操作, 即是 i -> 2*i
			然后,对 H[i+1] ( 相当于没有对i*2 之前的 H[2*i+1], 即H[i] 的右孩子)
			与 H[i] (相当于没有对 i = i*2 之前的 H[2*i] , 即 H[i] 的左孩子)
			二者比较大小,
			如果 H[i]>H[i+1] 左孩子> 右孩子,则 ,直接将 H[i/2] 与H[i]置换
			如果 H[i] < H[i+1] 左孩子 < 右孩子 , 则将 i = i+1 ,然后 H[i+1] -> H[i] 了,
			<
			但是 H[i/2] ,由于除法操作是整除的操作, 所以,H[i/2] 还是父节点不会改变,
			即 , 若 i = 4 , 4/2 = 2 , (4+1) /2 = 2 . 在这里,由于之前的 
			i = i*2 操作使得i的数值一定是一个偶数, 所以在 /2 之后,
			所指向的数组下表不会改变。
			>
			进行下表修正之后,H[i] 所标示的是 待置换的节点的子节点中最大数值的节点。
			而H[i/2]所指向的节点是待置换的父节点。
			一旦有 H[i/2] < H[i] 则通过swap操作即可实现位置的交换。

			当然,在这里不要忘了判断i+1 之后的数值是否超出了堆中元素个数n的可访问的范围
			*/

			if ( (i+1<=n)&&( H[i+1] > H[i]))
			{
				i = i+1 ;
			}

			if ( H[i/2] < H[i] )
				swap(H[i/2] , H[i] ) ;
			else 
				done = true ;
		}
	}
}



sift_down 算法分析: 在sift_down 操作中,元素每下移一次,就会进行两次的比较操作。如果移动置换操作成功,其所在节点层数就会 +1 ,

这两个比较分别是 {设 H[i] 是待置换的父节点, 分别是 

1.<H [i] 与 max{H[2*i] ,  H[2*i+1]}  即父节点与两个子节点最大的那个进行比较 :第一次比较> 

2.<max{ H[2*i ], H[2*i+1] }, 这两个子节点为了选出最大的那个节点而进行自行的比较:第二次比较> 

由此可知,sift_down 操作的时间复杂度为 O ( logn) , 同时用于两个节点之间互换关键字的时候,需要一个临时变量的空间,所以空间复杂度为O(1).



3.元素插入操作

  在堆操作中,为了将一个元素 x 加入到堆中,通常的做法就是,首先将记录堆中元素个数的变量数值增1操作之后,然后将带插入的元素 x 放置到堆的末端,

然后,此时我们可以知道 元素 被加入到了堆中的 最后一个元素位置,下标以 n 记录,那么我们只要调用之前实现的方法 sift_up 来对 H[n] 进行上移操作,

进行上移操作之后,会为 元素 x 安放到 H[1..n] 中的一个合适的位置,算法描述代码如下:


/**
算法 4 元素插入操作
输入: 表示堆的数组 H [] , 及插入元素之后的堆中元素总数n
输出: 插入元素 x 之后的堆,还有更新之后记录堆中元素个数的n数值(以指针格式存放的变量)
*/

template <class Type>
void insert ( Type H[] , int &n , Type x )
{
	n = n+1 ;
	/**
	首先,进行堆中元素计数器的更新操作	*/
	 
	H[n] = x ;
	//然后, 将元素安放到堆中的最后一个元素的位置

	sift_up(H , n ) ;
	//调用方法上移操作, 将对H[n] 处的元素进行比对、置换在堆中找到一个新的位置,
	//使得整个堆在 元素插入到最后位置之后, 重新调整恢复平衡
}

下面来分析一下,上述算法的复杂度,其实这个算法的时间复杂度完全依赖于在其中调用的 sift_up 方法的时间复杂度,即sift_up 在执行的时候,

向上比较的次数,在这个算法中决定了时间的复杂度,我们这里考虑一下最坏的情况,即对于 一个最大堆 H [1..n] ,它的最新插入到 H[n = n+1] 处

的元素 x > H[1] , H[1] = max( H [1..n] ), 因为 H [1..n] 在元素插入之前是一个平衡的堆。 

但是, x 在插入之后,需要调整,通过 sift_up 方法,它将会经过 logn 层的比较操作,n 个节点对应 log n 层的堆结构,并且每次比较之后

就会从当前所在层上升一层。所以完全从 H[n] 所在层到达 H[1] 所在的层,需要经过一共 logn 层的连续跳跃。所以上述算法的时间复杂度为 O(logn)。


而空间复杂度仍然是在置换两个节点之间的关键字的时候所申请的临时单个变量,即空间复杂度为 O(1) 。


5. 元素删除操作

为了删除堆中的元素 H[i] , 可用堆中最后一个元素来取代待删除的H[i] , 然后根据本删除元素和取代它的元素的大小,来对与H[n] 置换后的,H[i]节点处,

执行一个 sift_down 操作,来使得整个堆维持一个平衡的状态。删除操作描述如下:

/**
算法5 : 元素删除操作
输入: 数组 H[] , 即满足堆性质的H[] , 当前堆中元素个数 n , 被删除元素在堆H[] 中的下标
输出: 删除元素 H[i]之后重新达到平衡状态的堆 H[] , 和更新后,堆中元素个数计数变量 n
*/

template  <class Type>
void Delete ( Type H[] , int &n , int i )
{
	Type x , y ;
	x = H[i] ;
	y = H[n] ;

	n = n-1 ;

	if ( i <= n )
	{
		H[i] = y ;

		//在这里将待删除节点 H[i] 与 堆中最后的元素置换,
		//并且我们可以看到,在算法一开始的时候, n = n-1 语句,
		//从而我们可以知道,通过H[n] 再也访问不到被删除的元素y 即 H[n]。
		if ( y > x ) 
			/** 如果替换待删除节点的元素数值 > 被删除节点的数值,则需要对其进行上移操作
			反之,为了保证堆的平衡,需要对其进行下移操作
					 */
					 sift_up(H , i) ;
		else
			sift_down( H , i ) ;

	}
}


Delete 算法复杂度分析,时间复杂度是由 sift_up 和 sift_down 两个方法分别决定的,而两个算法分别各自的时间复杂度为 O(logn) ,所以对于Delete

算法来说,它的时间复杂度也是 O(logn) ,空间复杂度所需要的额外空间也是1个进行元素交换的空间,即O(1).


6. 在堆中删除关键字最大的元素结点

  这个方法是这样的,在最大堆中,关键字最大的元素通常是存放于根节点,因此可以方便地把这个节点进行删除,但是,如何实现在删除最大节点即,根节点之后,使得变得不平衡的堆重新变得平衡等相关的后续操作,是这个方法的主要功能所在,但是在本质上,在本方法中是调用上一个方法的特例。

其实对于我们实现相应方法的时候也是如此,即,我们先将其中最为底层的代码进行实现,然后其余的方法的实现完全是基于调用特定的底层方法的基础上给予实现。 实现delete_max 的方法的代码如下:

/**
算法6:删除关键字最大的元素
输入: 数组H[] , 即堆 H[] , 删除操作前堆中元素计数变量 n 
输出: 删除堆顶点 H[1] 之后继续达到堆平衡之后的堆 数据结构 H[] ,
删除堆中元素更新计数堆中元素个数的变量 n 数值, 然后将从堆中删除的元素
作为返回值进行返回
*/

template  <class Type>
Type delete_max ( Type H [] , int &n )
{
	//this is used for both max_heap and min_heap
	Type x ;
	x = H[1] ;
	//首先申请变量 x 用以存放即将被删除的元素节点对应的关键字

	Delete( H , n , 1 ) ;
	//然后调用Delete 方法, 将堆中的位于 1 处的关键字从堆结构中进行删除
	//将计数堆中节点的变量 n 传入,在堆中删除节点 1 ,并进行平衡之后,
	//同样也会更新 n 的数值,使之等于 n-1 的数值。

	return x ;
}


对算法 delete_max 算法复杂度的分析:

由于删除操作的时间复杂度 ,即Delete 方法的复杂度 = sift_down 或是 sift_up 的复杂度,为 O(logn)。


7.堆的建立

  现在,假定我们从一个空的堆开始,把数组中的n个元素,连续地使用 insert 操作插入到堆中,可以通过下述的 make_heap1的方法,

来调用前面的 insert 方法来创建一个堆结构。接下来,将要介绍一下,堆在建立的时候需要的代码:


/**
算法 7 : 建造堆的第一种算法

输入: 数组 H [] , 即堆对象, 和用于记录堆中的元素个数的变量 n
其中 数组 A [] 是用来存放即将被插入值堆中的元素数值, 变量 m 的初始值为0 ,
是用来作为记录构成的堆中元素个数的变量。 

输出: 含有 n 个元素的堆 H[] 
*/

template < class Type >
void make_heap1( Type A[] , Type H [] , int n )
{
	int i , m = 0 ;
//首先, 将数值m 置为数值 0 , 用它来记录堆中的元素个数的多少,
//然后,创建一个循环,在循环中每次循环将会访问数组A中的一个变量,
//然后将提取出来的变量值作为参数通过 insert 方法作为堆的关键字传入至创建的堆中

	for ( i = 0 ; i < n ; i++ )
	{
		insert ( H , m , A [i]) ;
	}
}

好的, 在介绍完构件堆的算法之后, 我们来了解一下这个算法的时间,空间的复杂度,首先是时间复杂度,我们根据前面的代码可以知道,

每进行一次 insert 操作, 对应都会在内部执行一个 sift_up 的操作,我们根据前面的代码可以知道,它的时间复杂度是 O(logn) , 那么创建一个里面

含有 n 个元素的数据堆的话,需要调用 n 次 insert 方法,即会使得时间复杂度达到 O(nlogn) 。大致过程是这样的,在插入第 i 个元素的时候,先把这个元素放在堆的末端,这个时候它处于的是堆中的 第 (int)(logi) 层,那么它会花费 O(logi) 的时间进行上移操作。 故插入 n 个元素之后,所需要的执行时间复杂度为 O(nlogn) 。 对于空间复杂度是这样的, 因为在堆创建之前需要将插入到堆中的元素先存放到一个可以存放n个变量的数组空间中,所以,空间复杂度为 O(n). 当然,也是可以这样理解的, 即一个 insert 方法所消耗的空间复杂度为 O( 1 ) 那么在循环中调用 n 次insert 方法的话,所需要消耗的空间

复杂度 为 O ( n ) .


8. 通过调整数组来构建一个堆结构

当然,上述的方法,在本质上是这样的, 即额外分配一个数组空间 A 然后将A中的数据转移到 H 中, 然后再将 H 转换为一个堆结构, 可以这样来改进算法, 即我们不通过中间的存储变量 A, 实现一个算法,直接将数据存放于 数组 H [] 中, 然后对数组 H [] 运行相应的算法, 使得 H [] 满足堆平衡的性质,

则将H[] 从一个普通数组转换成一个满足堆性质的堆。

即将数组本身构造成一个堆, 调用的过程是从最后一片树叶,找到它上面的分支节点, 从这个分支节点开始进行下移操作,下一操作一直进行到根节点为止, 如果对于每一个新插入的节点都进行如下操作的话,最后会使得数组构成一个堆结构。

下面的算法 make_heap 代码描述的就是一个这样的过程:

/**
算法 8 建造对的第二种算法
输入: 数组 H[] , 和想要构建的堆数据结构中元素的个数 n
输出: 以堆性质所定义的堆的数据结构 H [] 
*/

template <class Type>
void make_heap( Type H [] , int n )
{
	int i ;
	H[n] =H [0] ;

	for ( i = n/2 ; i >= 1 ; i--)
	{
		sift_down( H ,n ,  i ) ;
	}
}
/**
这个算法在调整数组到堆的思想是这样的,首先将位于下表为0 的数据
存放到堆中的最后一个元素的地方,以为使用数组存放数据的下表是从0 开始的,
而使用堆存放数据的下标是从 1 开始的。所以将数组转换为堆的话,首先应该将数组中的
位置[0] 的元素提取出来。

接下来的循环的思想是这样的,首先将 i = n/2 ,即我们对堆的遍历是从堆中最后一个元素
的父节点开始对每个节点执行 sift_down 操作, 
sift_down ( H 将会比较{ H [ i ] , H [2*i] , H[2*i+1] }三个元素的大小,并做相应的交换
的操作,当然前提是 2*i+1 <= n 这样才会在堆访问堆中元素节点的范围之内。
最后,执行的操作将会发生在 {H [1] , H[2] , H[3] 开始直至 H[(n-1)/2] , H[n-1] , H[n]}
经历上述操作之后,H[] 就会从一个数组转换成一个堆了
*/

下面分析一下算法的时间复杂度:

1,首先我们可以假定数组中一共有 n 个元素,则由这 n 个元素所构成的二叉树(完全二叉树) 高度为 h = [logn] ( [ ]  在这里指代的是下取整)

2, 对于处于第 i 层上的元素  H[i] 执行下移操作,那么它最多向下移动 h-i 层,达到整个堆结构的底部,并且每向下移动一层就会进行 2 次元素比较,

 这里所说的 2 次比较指的是 1 次 子节点互相比较选出数值最大的节点, 2 次 是父节点与第一次比较产生的最大节点的比较过程。一旦小于则会进行互换位置的操作。因此可以知道,对于第 i 层上的每个元素所执行下移操作,最多执行 2(h-i) 次元素比较。

3. 根据二叉树的结构可知,第 i 层( i < h ) 是满枝的, 那么该层上的节点总数是 2^i 个 , 因此如果对于第 i 层上的所有节点都执行下移操作的话,最多执行元素比较的次数是 2(h-i)*2^i 。


4. 第 h 层上的元素如果都是叶子节点的话, 那么它是无需执行下一操作的, 这也是为什么执行 sift_down 方法的其实节点 为 n/2 , 因为对于一个有 n 个结点的完全二叉树来说, n/2 是第一个非叶子节点的下表,也就是从堆中最后一个元素开始计数第一个不是叶子节点的节点。

因此,最多只需要 对 0 层到 k-1 层的元素执行下移操作即可。

最终得出算法的时间复杂度为  O(n) ,并且它是优化了第一种通过额外数组 A 来创建堆数据结构的方式,所以将空间复杂度降低到 O(1) .





在这里我们令 n = 2^k , 即得到   k = logn , 这样上述的代数式通过就会转换成



=2(k2^k - k ) - 2(k2^k - 2^(k+1)+2 )

= 4n - 2logn - 4 

 < 4n  的代数式 , 从而可以推断,算法make_heap 的执行时间是 O(n) .



第三部分:

在这一部分,将针对STL中堆的使用方法,给出相应的代码,对于STL的学习,虽说透过表面现象了解事物的本质是LZ一直觉得很好的学习方法,

但是如果想要了解STL内部实现细节的话,首先还是要先学会使用STL工具,带到在你自己写的代码中STL中提供的方法已经使用的熟悉之后

再来学习STL内部实现机制也不晚。

第三部分主要分为2个部分,

3.1 将会简单介绍一下在STL中与堆有关的接口方法都有哪些,并且它们内部的工作机制 

3.2 将会介绍使用前一段介绍的方法针对一般类型的实现 



3.1    STL中常用到的堆方法:

判断堆:

1. bool is_heap( iterator start , iterator end ) ;

这个是用来判断对于迭代器 start 和 end  所指向的区间 [start , end] 中的数据能否构成 一个合法的堆,

如果判断可以构成一个合法的堆数据结构,则会返回 true , 如果判断出这个区间上的数据是无法满足构成堆的条件,

则会返回 false .

2. bool is_heap( iterator start , iterator end , StrictWeakOrdering cmp ) ;

这个方法是用来判断由start 和 end 所指向的区间 [start , end ] 中的元素是否在 cmp 的条件限制下,仍旧满足生成堆的条件。

如果满足返回 true , 不满足返回 false .


构造堆:

1. void make_heap( iterator start , iterator end ) ;

2. void make_heap ( iterator start , iterator end , StrictWeakOrdering cmp ) ;

这个方法是用来,在迭代器所指定的区间 [start , end ] 内生成一个堆, 默认在不对其传入特殊限定条件的情况下,

生成的是大顶堆, 即 max-heap .

当使用方法2 来生成堆结构的时候,可以通过传入的由用户自定义的 cmp 函数来设定生成的堆,究竟是大顶堆还是小顶堆。


弹出元素:

1. void pop_heap( iterator start , iterator end ) ;

2. void pop_heap( iterator start , iterator end , StrictWeakOrdering cmp ) ;

在这里需要知道的是, 调用pop_heap 方法之后,并不是对堆顶的元素进行删除,然后弹出操作,

而是执行如下的步骤 1. 首先将堆顶的元素与堆中最后的元素进行互换。 2. 然后,调整堆中元素的访问范围,

通过减少堆中元素的个数,即n-- ,来是堆操作不会访问到n-- 操作之前的堆中的最后一个元素。3. 然后对失去平衡的堆

调用相关算法使之重新达到平衡状态。

通常如果想要删除堆中的最后那个元素的话,可以调用pop_back()这一方法,将原先堆中最后一个元素删除即可。


当然对于pop_heap 中的第二个方法, 即传入 cmp 方法是可以将用户自定义的规则传入值方法中,即在置换元素之后,

是想要将堆调整成是大顶堆还是小顶堆的堆的方式。


通常情况下,pop_heap 是与 pop_back 操作方法联合使用的。


向堆中压入元素

1. void push_heap( iterator start , iterator end ) ;

2. void push_heap ( iterator start , iterator end , StrictWeakOrdering cmp ) ;

这个方法通常是和push_back 来配合着工作的,通常我们使用push_back 将元素压入到堆中的最后一个元素的地方,

这个操作将会是迭代器 end 的数值增加1 , 然后,在调用 push_heap()方法即可。

在上述过程中push_heap 方法所实现的功能, 是将最后一个元素在整个堆中找到一个合适的位置进行安放,然后重新

整合堆,使之重新按照堆的性质来排列整个堆中的元素。当然,在有新元素插入,然后整合成一个新的堆默认的情况

是重新整合成一个大顶堆。但是对于第2 种方法,我们可以通过传入函数 cmp 来设定重新整合的堆是大顶堆还是小顶堆。


堆排序:

1. void sort_heap( iterator start , iterator end ) ;

2. void sort_heap( iterator start , iterator end , StrictWeakOrdering cmp ) ;


这两种方法都是根据迭代器 start 和 end 所指定的区间 [start , end ] 来对其进行排序, 通常我们知道,

使用构建一个堆数据结构是如何达到排序的目的的: 

如果实现从大到小排序的话, 那么构建一个小顶堆,

如果实现数据的从大到小排序的话,那么构建一个大顶堆即可。

指定构建的大顶堆和小顶堆可以通过方法 2 中的传入参数 cmp 来对其进行指定。


当然在这里需要注意的有一点就是,在构建即make_heap 以及 pop_heap ,push_heap 的时候,如果构建的是

大顶堆(小顶堆的话) 那么在排序的时候根据之前的设定进行排序,如果之前一直都是指定是小顶堆的话,在排序的时候

通过第二种传递函数的方法执行以大顶堆的方式排序,会造成一些使得程序死锁的错误。


3.2 测试代码:

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional> //this is for the greater<> less<>

using namespace std ;

int main ()
{

	int number [10] = {2 , 1 , 10 , 8 , 90 , 30 , 9 , 98 , 6 , 7} ;
	vector<int> v(number , number+10 ) ;

	//make_heap( v.begin() , v.end() ) ;
	//this will make the stack the first one is the largest one , 
	//others not sure
	//cout<<"Initial Max heap:"<<v.front() <<endl ;
	//make_heap(v.begin() , v.end() , less<int>()) ;
	//this will make the stack the first element is the largestone
	//cout<<"Initial Max heap :"<<v.front() <<endl ;

	make_heap(v.begin() , v.end() , greater<int>() ) ;
	//the first one is the smallest one , others not sure
	cout<<"Initial Min heap :"<<v.front() << endl;



	//pop_heap(v.begin() , v.end() ) ;
	//after pop , rebuilt the stack into the mix-stack
	//v.pop_back() ;
	//here we pop out the last element in the stack

	//pop_heap(v.begin() , v.end() , less<int> () ) ;
	//after pop rebuilt into the mix-heap 
	//v.pop_back() ;
	//here we pop up the last one in the stack 



	pop_heap(v.begin() , v.end() , greater<int>() ) ;
	//after pop , rebuilt into the min-heap

	v.pop_back() ;
	//here we pop up the last on in the stack 
	cout<<"Min heap after pop: " <<v.front() <<endl ;

	v.push_back(80) ;
	//put to the last one 

//	push_heap(v.begin () , v.end() ) ;
	//Keep the first on is the largest one
//	push_heap(v.begin() , v.end() , less<int> () ) ;
	//Keep the first on is the largest one 

//	cout<<"max-heap after push :"<<v.front() <<endl ;

	push_heap(v.begin() , v.end() , greater<int>() ) ;
	//keep the first one is the smallest one

	cout<<"min heap after push :"<<v.front() <<endl ;

	//sort_heap(v.begin() , v.end() ) ;
	//sort by descent
//sort_heap(v.begin() , v.end() , less<int> () ) ;

	sort_heap(v.begin() , v.end() , greater<int> () ) ;

	cout<<"final sorted range" ;

	for (int i = 0 ; i < v.size() ; i++ )
		cout<<" "<<v[i] ;

	cout<<endl ;


	system("pause");

	return 0 ;
}

第四部分

在掌握了堆的基本原理(用于根据ACM题中的论述来分析解题时基于的思想) 和如何使用STL中提供的快捷堆操作(用于在实际使用中快捷方便的使用)之后,

我们可以试着解答两道ACM的题目:

poj 2010 & poj 2823 & poj2442 & poj3481 & poj2051


今天先到这里,有时间继续~




















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值