您的位置:首页 > 其它

排序(三) 堆排序

2014-04-20 23:24 746 查看
堆排序

堆排序算法是 Floyd 和 Williams 在1964 年共同发明的,同时,他们发明了“堆”这样的数据结构。
引子:叠罗汉
定义:堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子节点的值,称为大项堆;或者每个结点的值都小于或等于其左右孩子节点的值,成为小项堆。



这里需要注意的是:根节点一定是堆中所有节点最大(小)者。
堆排序思路:将线性表排列成大顶(或者小顶)堆,然后将堆顶元素(当前最大值)与最后一个元素交换,然后将去除最后元素的其它元素重新调整为大顶堆,重复之前的操作,直到最后一个元素。

代码如下:

/* 已知L->r[s..m]中记录的关键字除L->r[s]之外均满足堆的定义 */
/* 本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆 */
/* 通俗化理解:将一串数字进行从中间往前的最小树根排序过程,下例中,将 下标为 length/2 至 1 的元素进行最小树倒着排序 */
/*  
		
	        50                        50                  
         /      \                   /     \
	    10       90                10       90 
       /  \     /  \      ->      /  \     /  \     ->    
      30  70   40   80           60   70  40  80
	 /  \                       /  \ 
    60   20                    30  20  (第一次排序,L = length/2 ,在本例中,就是 第四个节点,即 60,调整排序)

			50                                      50
         /       \                               /       \  
       10        90                             70        90
->     /  \      / \             ->            /   \      /  \
      60   70   40  80                        60    10   40   80
     /  \                                    /  \ 
    30   20(第二次排序,对 90、40、80排序)  30   20(第三次排序,i--,对以70为根的二叉树调整顺序)
   
             90         
         /        \
        70        80
->     /  \      /  \
      60  10    40   50
     /  \ 
    30   20(第四次排序,对以50为根的二叉树调整顺序,50与90交换,50再与80交换,最后完成)   
*/
void HeapAdjust( SqList *L, int s, int m)
{
	int temp, j;
	temp = L->r[s];
	for( j = 2*s; j <= m; j*=2 )	//j=j*2的递增,是为了判断做过替换的j是否有孩子,如果有再作调整,没有则完成。
	{
		/* 沿关键字较大的孩子结点向下筛选 */
		if( j < m && L->r[j] < L->r[j+1] )
			++j;	/* j为关键字中较大的记录的下标 */
		if( temp >= L->r[j] )
			break;	/* 没有破坏原堆的性质,终止for循环 */
		L->r[s] = L->r[j];
		s = j;
	}
	L->r[s] = temp;
}

/* 我们所谓的将待排序的序列构建成一个大顶堆,其实就是从下往上、从右到左,将每个非终端节点(非叶节点)*/
/* 当作根节点,将其和其子树调整为大顶堆。 */
/* 对顺序表L进行堆排序 */
void HeapSort(SqList *L)
{
	int i;
	for( i = L->length/2; i>0; i--)		/* 把 L 中的 r 构建成一个大顶堆 */
		HeapAdjust(L, i, L->length);
	
	for( i = L->length; i>1; i--)
	{
		/* 将堆顶记录和当前未经排序子序列的最后一个记录交换 */
		swap(L, 1, i);	
		/* 将L->r[1...i-1]重新调整为大顶堆 */
		HeapAdjust(L, 1, i-1);
	}
}


复杂度分析

它的运行时间主要是消耗在初始构建堆和在重建堆的反复筛选上。
在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和如有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。
在正式排序时,第 i 次去堆顶记录重建堆需要用O(long i )的时间(完全二叉树的某个结点到根节点的距离为[log2 i ]+1),并且需要取 n-1 次堆顶记录, 因此, 重建堆的时间复杂度为O(n log n)。
所以,总体来说,堆排序的时间复杂度为O(n log n)。
由于堆排序对原始记录的排序状态并不敏感,因此无论是最好、最坏和平均时间复杂度均一样。这在性能上显然要远远好于冒泡、简单选择、直接插入的o(n^2)的时间复杂度了。
空间复杂度上,它只有一个用来交换的暂存单元,也非常的不错。不过由于记录的交换与比较是跳跃式进行,因此堆排序也是一种不稳定的排序方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: