堆排序思想

在直接选择排序中,待排序的数据元素集合构成一个线性结构,要从有n个数据元素的线性结构中选择出一个最小的数据元素需要比较n-1次。如果能把待排序的数据元素集合构成一个完全二叉树结构,则每次选择出一个最大(或最小)的数据元素只需比较完全二叉树的深度次即lbn次,则排序算法的时间复杂度就是O(nlbn)。这就是堆排序的基本思想。

大根堆的定义

设数组a中存放了n个数据元素,数组下标从0开始,如果当数组下标2i+1<n时有a[i].key≥a[2i+1].key,当数组下标2i+2<n时有a[i].key≥a[2i+2].key,则这样的数据结构称为最大堆。

如果把有n个数据元素的数组a中的元素看作一棵完全二叉树的n个结点,则a[0]对应着该完全二叉树的树根,a[1]对应着树根的左孩子结点,a[2]对应着树根的右孩子结点,a[3]对应着a[1]结点的左孩子结点,a[4]对应着a[1]结点的右孩子结点,如此等等。在此基础上,只需再调整所有非叶结点的数组元素,使之满足条件:a[i].key≥a[2i+1].key和a[i].key≥a[2i+2].key,则这样的完全二叉树就是一个最大堆。

cnnlstm的数据结构 数据结构lbn_数组

小根堆的定义

类似地,最小堆的定义如下:设数组a中存放了n个数据元素,数组下标从0开始,如果当数组下标2i+1<n时有a[i].key≤a[2i+1].key,当数组下标2i+2<n时有a[i].key≤a[2i+2].key,则这样的数据结构称为最小堆。根据堆的定义可以推知,堆有如下两个性质。
① 最大堆的根结点是堆中值最大的数据元素,最小堆的根结点是堆中值最小的数据元素。
② 对于最大堆,从根结点到每个叶结点的路径上,数据元素组成的序列都是递减有序的;对于最小堆,从根结点到每个叶结点的路径上,数据元素组成的序列都是递增有序的。例如,对图(b)所示的最大堆,根结点元素是堆中值最大的数据元素;从根结点到4个叶结点的路径上,数据元素组成的4个序列都是递减有序的。通常称堆的根结点元素为堆顶元素。

创建堆

要进行堆排序,首先要创建堆。按非递减序列排序时,要创建最大堆。设数组a中存放了n个数据元素,若把数组a中这n个数组元素看作是一棵完全二叉树的n个结点,则这棵有n个结点的完全二叉树采用了顺序存储结构。但是完全二叉树还不一定满足最大堆的定义。要让一棵完全二叉树满足最大堆的定义,需要从完全二叉树的叶结点端开始逐个结点进行调整,使它们满足最大堆的定义。

在一棵按顺序存储结构存储的完全二叉树中,所有叶结点都满足最大堆的定义。对于第1个非叶结点a[i](i = (n-2)/2)(注意:此处的符号“/”表示整除),由于其左孩子结点a[2i+1]和右孩子结点a[2i+2]都已是最大堆,所以只需首先找出a[2i+1]结点和a[2i+2]结点的较大者,然后比较这个较大者结点和a[i]结点。如果a[i]结点大于或等于这个较大的结点,则以a[i]结点为根结点的完全二叉树已满足最大堆的定义;否则,对换a[i]结点和这个较大的结点,对换后,以a[i]结点为根结点的完全二叉树满足最大堆的定义。

按照这样的方法,再调整第2个非叶结点a[i-1],第3个非叶结点a[i-2],……,直至最后调整根结点a[0]。当根结点调整完后,这棵完全二叉树就是一个最大堆了。当要调整结点的左、右孩子结点是叶结点时,上述调整过程非常简单。当要调整结点的左右孩子结点不是叶结点时,上述调整过程要稍微复杂一些。因为这时a[i]结点的值可能很小,a[i]结点与a[2i+1]结点和a[2i+2]结点的较大者对换后,可能会使完全二叉树不满足最大堆的定义,从而引起一连串的调整过程。

例如,设数组a中存放的数据元素依次为:10, 50, 32, 5, 76, 9, 40, 88,对应的完全二叉树如图(a)所示,其调整过程如图(b)、(c)、(d)、(e)所示。其中,图(b)是第1个非叶结点(即下标为i = (n-2)/2 = (8-2)/2 = 3的数组元素)调整完后的状态,此次调整交换了88和5的位置;图(c)是第2个非叶结点调整完后的状态,此次调整交换了40和32的位置;图10-5(d)是第3个非叶结点调整完后的状态,此次调整交换了88和50的位置;图(e)是根结点调整完后的状态。当调整根结点时,将引起数据元素88和数据元素76的上移,数据元素10最终将下移存放在原数据元素76的位置。经过上述调整后,完全二叉树就变为最大堆了

cnnlstm的数据结构 数据结构lbn_完全二叉树_02


cnnlstm的数据结构 数据结构lbn_结点_03


因为完全二叉树可以直接存储在数组中,把完全二叉树调整为最大堆的过程也就是把数组中的元素按照最大堆的要求进行调整的过程,所以这样的过程也称作数组的最大堆化

建大根堆的算法

当完全二叉树中某个非叶结点a[h](h = (n-2)/2)的左孩子结点a[2h+1]和右孩子结点a[2h+2]都已是最大堆后,调整非叶结点a[h]使之满足最大堆的函数如下:

void CreatHeap(DataType a[],int n,int h){// 调整非叶节点a[]使之满足最大堆,n为数组a的元素个数 
	int i,j,flag;
	DataType temp;
	
	i = h;//i为要建堆的二叉树根结点 
	j = 2*i+1;//j为i的左孩子结点的下标 
	temp = a[i];
	flag = 0; 
	
	while(j<n&&flag !=1){//沿左右孩子中,值较大者重复向下筛选 
		if(j<n-1 && a[j].key <a[j+1].key){
			j++;
		} 
		
		if(temp.key > a[j].key){
			flag = 1;
		}else{
			a[i] = a[j];
			i = j;
			j = 2*i+1;
		}
	}
	
	a[i] = temp;//把最初的a[i]赋予最后的a[j] 
}

利用上述CreatHeap(a, n, h)函数,初始化创建最大堆的过程就是从第1个非叶结点a[h](h = (n-2)/2)开始,到根结点a[0]为止的循环调用CreatHeap (a, n, h)函数的过程。

初始化创建最大堆算法如下:

void InitCreatHeap(DataType a[], int n){//把数组元素a[]初始化创建为最大堆 
	int i;
	
	for(i=(n-2)/2; i>=0; i--){
		CreatHeap(a,n,i);
	} 
}

堆排序的基本思想

首先把有n个元素的数组a初始化创建为最大堆,然后循环执行如下过程直到数组为空为止:
① 把堆顶a[0]元素(为最大元素)和当前最大堆的最后一个元素交换;
② 最大堆元素个数减1;
③ 由于第①步后根结点不再满足最大堆的定义,所以调整根结点使之满足最大堆的定义。

堆排序算法如下:

void HeapSort(DataType a[], int n){//堆排序堆数组a[]进行排序 
	int i;
	DataType temp;
	
	InitCreatHeap(a,n);//初始化创建最大堆
	
	for(i = n-1; i>0; i--){//当前最大堆个数每次递减1 
		//把堆顶a[]元素和当前最大堆的最后一个元素交换
		temp = a[0];
		a[0] = a[i];
		a[i] = temp; 
		
		CreatHeap(a,i,0);//调整根节点满足最大堆;此时子二叉树根节点下标为0,子二叉树节点个数为i 
	} 
}

测试

#include <stdio.h>
#include <stdlib.h>

typedef int KeyType; 

typedef struct{
	KeyType key;
}DataType;

void CreatHeap(DataType a[],int n,int h){// 调整非叶节点a[]使之满足最大堆,n为数组a的元素个数 
	int i,j,flag;
	DataType temp;
	
	i = h;//i为要建堆的二叉树根结点 
	j = 2*i+1;//j为i的左孩子结点的下标 
	temp = a[i];
	flag = 0; 
	
	while(j<n&&flag !=1){//沿左右孩子中,值较大者重复向下筛选 
		if(j<n-1 && a[j].key <a[j+1].key){
			j++;
		} 
		
		if(temp.key > a[j].key){
			flag = 1;
		}else{
			a[i] = a[j];
			i = j;
			j = 2*i+1;
		}
	}
	
	a[i] = temp;//把最初的a[i]赋予最后的a[j] 
} 

void InitCreatHeap(DataType a[], int n){//把数组元素a[]初始化创建为最大堆 
	int i;
	
	for(i=(n-2)/2; i>=0; i--){
		CreatHeap(a,n,i);
	} 
}

/*
堆排序 
*/
void HeapSort(DataType a[], int n){//堆排序堆数组a[]进行排序 
	int i;
	DataType temp;
	
	InitCreatHeap(a,n);//初始化创建最大堆
	
	for(i = n-1; i>0; i--){//当前最大堆个数每次递减1 
		//把堆顶a[]元素和当前最大堆的最后一个元素交换
		temp = a[0];
		a[0] = a[i];
		a[i] = temp; 
		
		CreatHeap(a,i,0);//调整根节点满足最大堆;此时子二叉树根节点下标为0,子二叉树节点个数为i 
	} 
}


void main(void){
	int i=0;
	DataType array[8]={10,50,32,5,76,9,40,88};
	DataType array2[8]; 
	printf("原数组顺序:");
	for(i=0;i<8;i++){
		printf("%d ",array[i]);
	}
	printf("\n\n");
	HeapSort(array,8);
	
	printf("最大堆排序之后结果:");
	for(i=0;i<8;i++){
		printf("%d ",array[i]);
	} 
	getch(0);
}

堆排序的过程

cnnlstm的数据结构 数据结构lbn_cnnlstm的数据结构_04

复杂度计算

堆排序算法是基于完全二叉树的排序。把一个完全二叉树调整为堆,以及每次堆顶元素交换后进行调整的时间复杂度均为O(lbn),所以,堆排序算法的时间复杂度为O(nlbn)
堆排序算法的空间复杂度为O(1)。观察上述例子即可发现,堆排序算法是一种不稳定的排序方法