【数据结构与算法】堆排序

堆排序

优先队列(priority queue)是按照某种优先级进行排列的队列,优先级越高的元素出队越早,优先级相同者按照先进先出的原则进行处理。优先队列的基本算法可以在普通队列的基础上修改而成。例如,入队时将元素插入到队尾,出队时找出优先级最高的元素出队;或者入队时将元素按照优先级插入到合适的位置,出队时将队头元素出队。这两种实现方法,入队或出队总有一个时间复杂度为 O ( n ) {O(n)} O(n)。而采用堆来实现优先队列,入队和出队的时间复杂度则均为 O ( l o g 2 n ) {O(log_{2}n)} O(log2n)

堆的定义

堆(heap)可以定义为一棵二叉树,树的节点中包含键(每个节点一个键),并且满足下面两个条件:

(1)树的形状(shape property)要求,这棵二叉树是基本完备(essentially complete)的(或者简称为完全二叉树),这意味着,树的每一层都是满的,除了最后一层最右边的元素有可能缺位。

(2)父母优势(parental dominance)要求,又称为堆特性(heap property),每一个节点的键都要大于或等于它子女的键(大根堆),或者每一个节点的键都要小于或等于它子女的键(小根堆)(对于任何叶子我们认为这个条件都是自动满足的)。

如果将堆按层序从0开始编号至n结束,则结点之间满足以下关系:

在这里插入图片描述

从堆的定义可以看出,一个完全二叉树如果是堆,则根节点(称为堆顶)一定是当前堆中所有节点的最大者(大根堆)或最小者(小根堆),如下图所示。用小根堆实现的优先队列称为极小队列,用大根堆实现的优先队列称为极大队列。

在这里插入图片描述

堆的构造

堆的逻辑结构为完全二叉树,而堆的存储结构一般用数组进行存储。以大根堆为例,可以把堆定义为一个数组 H [ 0.. n ] H[0..n] H[0..n],其中,数组前半部分中,每个位置i上的元素总是大于等于位置2i+1和2i+2中的元素,也就是存在如下关系:
H [ i ] ≥ m a x { H [ 2 i + 1 ] , H [ 2 i + 2 ] } i = 0... ⌊ ( n + 1 ) / 2 − 1 ⌋ H[i] \ge max\{H[2i+1],H[2i+2]\} \\ i=0...\lfloor{(n+1)/2-1}\rfloor H[i]max{ H[2i+1],H[2i+2]}i=0...(n+1)/21
根据这一特性,构造一个堆的方法主要有两种:

  1. 自底向上堆构造
  2. 自顶向下堆构造

自底向上堆构造

在初始化一棵包含n个节点的完全二叉树时,我们按照给定的顺序来放置键,然后按照下面的方法对树进行“堆化”。从最后的父母节点开始,到根为止,该算法检查这些节点的键是否满足父母优势要求。如果该节点不满足,该算法把节点的键K和它子女的最大键(最小键)进行交换,然后再检查在新位置上,K是不是满足父母优势要求。这个过程一直继续到对K的父母优势要求满足为止(最终它必须满足,因为对于每个叶子中的键来说,这个条件是自动满足的)。对于以当前父母节点为根的子树,在完成它的“堆化”以后,该算法对于该节点的直接前趋进行同样的操作。在对树的根完成这种操作以后,该算法就停止了。

在这里插入图片描述

// 堆构造
template<typename T>
void HeapBottomUp(vector<T>& H){
	int n = H.size();
	for(int i=n/2-1; i>=0; i--){
		int k=i;
		T v = H[k];
		bool heap = false;
		while(!heap && 2*k+1<n){
			int j=2*k+1;
			if(j+1<n && H[j] < H[j+1]){// 存在两个子女 
				j = j+1;
			}
			if(v>=H[j]){
				heap=true;
			}else{
				H[k]=H[j];
				k=j;
			}
		}
		H[k]=v;
	}
}

自顶向下堆构造

自顶向下算法(效率较低)通过把新的键连续插入预先构造好的堆,来构造一个新堆。首先,把一个包含键K的新节点附加在当前堆的最后一个叶子后面。然后按照下面的方法把K筛选到它的适当位置。拿K和它父母的键做比较:如果后者大于等于K,算法停止(该结构已经是一个堆了);否则,交换这两个键并把K和它的新父母做比较。这种交换一直持续到K不大于它的最后一个父母,或者是达到了树的根为止。

在这里插入图片描述

堆顶的删除

从一个堆中删除根的键。要删除的键和最后的键做交换,然后,我们这样来“堆化”(堆调整)这棵较小的树:根中的新键和它子女中较大的键做交换,直到满足父母优势要求,具体过程如下图所示。

在这里插入图片描述

// 堆调整
template<typename T>
void adjustHeap(vector<T>& H, int k, int len){
	T temp = H[k];
	for(int i=2*k+1; i<len; i=2*i+1){
		if(i+1<len && H[i] < H[i+1]){
			i++;
		}
		if(H[i] > temp){
			H[k] = H[i];
			k = i;  
		}else{
			break;
		}
	}
	H[k] = temp; 
}

堆排序

堆排序(heapsort)是威廉姆斯(J.W.J. Williams)发明的一种重要的排序算法,这种两阶段算法的实现过程分为两个部分:

第一步:构造堆,即为一个给定的数组构造一个堆。

第二步:删除最大键,之后进行堆调整,并循环执行第二步直至删除所有元素。

最终结果是按照降序删除了该数组的元素。但是对于堆的数组实现来说,一个正在被删除的元素是位于最后的,所以结果数组将恰好是按照升序排列的原始数组。

在这里插入图片描述

图中第一步构造堆的过程使用了自底向上的构造方式,将数组构造成大根堆!

在这里插入图片描述

图中第二步中,每一次都将堆顶与最后一个元素交换(删除最大键),并对剩下的部分(前面未被删除的元素)进行堆调整,如此循环多个回合后,得到从小到大顺序排列的数组。

实际上,无论是最差情况还是平均情况,堆排序的时间效率都属于 O ( n l o g n ) O(nlogn) O(nlogn)。因此,堆排序的时间效率和合并排序的时间效率属于同一类。而且,与后者不同,堆排序是在位的,也就是说,它并不需要任何额外的存储空间。针对随机文件的计时实验指出,堆排序比快速排序运行得慢,但和合并排序相比还是有竞争力的。

代码实现

template<typename T>
void HeapSort(vector<T>& H){
	// 第一步:建堆 
	HeapBottomUp(H);
	// 第二步:删除最大键,并进行堆调整 
	for(int i=H.size()-1; i>0; i--){
		swap(H[0], H[i]); // C++中提供了swap函数 
		adjustHeap(H, 0, i);
	} 
}

猜你喜欢

转载自blog.csdn.net/zzy_NIC/article/details/121183809