堆排序以及其应用大总结

教材上很详细,网上内容也不少,但感觉不够直观、简练、丰富。下面按照自己方式总结下。

提纲:

1)算法描述

2)代码

3)“三围”以及证明(复杂度、效率、稳定性等分析)

4)算法直接应用

5)算法原理应用

6)举例


一、算法描述:

    堆概念(数据结构):堆是一颗完全树,同时满足每个节点均大于或小于它的子节点,这样的数据结构被称为最大堆或者最小堆。很多博客里面说是一个完全二叉树,实际上三叉、四叉也是可以的,只不过对数运算曲线变化很快,2叉就足够了,一般只使用二叉就足够了,而且这样编写算法也容易很多。这个很好理解,10亿约等于2的30次方,即10亿个数之通过30次就可以求解,因此就没必要搞三叉了。常见的堆有二叉堆斐波那契堆等。

    堆的特点:这种结构处于一种半排序状态,它的存取效率从整体上讲介于有序和无序之间,具体可以参考本特利的《算法珠玑》中堆章节关于有序序列、无序序列和堆在操作上的复杂的分析。当对象处于动态时,是种非常有优势的数据结构。存取的时间复杂为log(a,n),其中a为完全树的度, 理解这种特点对于它的应用很有帮助。

    二叉堆存储以及规律:通常二叉堆以数组存储,且父子节点的下标存在如下关系:父节点(下标为:i)的左孩子节点下标为:2i,右孩子下标为:2i+1,另外堆顶元素的下标为 1,层为 log(2,n)。注:下标以1开始计算。

   

    堆排序思路:对于一个堆(以下皆以二叉最小堆说明),去掉它的堆顶元素,然后以最末端元素移动到堆顶位置,然后进行调整,使之再次成为最小堆。如此迭代,直到没有剩余的元素,依次取出来的顺序就是实现排序的过程。表达成伪代码即:

    建堆;

    循环(条件:堆不为空){

      取出堆顶元素;

      将最后一个元素移动到堆顶位置;

      调整使之再次成为堆;

    }

    

    建堆思路:从最后一个非叶子节点开始,按照从下到上的顺序进行迭代,让它(下标记为x)同其孩子节点(下标:2x、2x+1)比较,如果满足小于任何一个孩子节点,则说明这个子树是符合堆规则的;否则把其它同其最小的子节点交换。由于交换后,以这个节点(x)会破坏原来孩子的堆特性,因此这里有一个子迭代,让它继续上面的行为,直到找到一个合适的位置落脚。如此,直到根节点,就可以保证整颗完全树具备堆的性质。具体见图片,这里面描述的是大根堆的建堆过程,小根堆是一样的。就偷个懒吧 大笑


    【?】

        1> 仔细观察和思考后,我们可以发现其实建堆和堆调整可以提取一个公共的方法来。就是在子树具备堆特性的条件下,可以使用相同的方法进行调整。(1个根节点可以认为是一颗特殊的堆,即使最大堆也是最小堆。)

        2> 思考下为什么采用自底向上的方法进行迭代,反过来行不行??? 尝试下就会发现这么做的好处。

        3> 思考下什么情况下,会发生子迭代,即循环跟其孩子节点、孙子节点等所有后代进行比较的情况。


二、代码(自己开发的,基于严蔚敏老师的《数据结构》思路的算法,已经编译并测试通过。)

//program name	: heap sort
//author		: Dam
//email			: zhq651@126.com
//discription	: This is a sort that fits for find max or min key from huge data;
// The tree mentioned before is binary tree.And it is not stabled.
// step 1:build a heap
// step 2:delete the root node;
// step 3:rebuild the heap by using the left nodes
// step 4:loop step 2 and step 3,until there is no node left.
//space & time:
// space: S(n)= O(1)
// time:  T(n)= nlog(2,n)


#include <stdio.h>
#include <time.h>

#define FALSE 0
#define TRUE 1
#define NUMMAX 1000000

typedef struct hs_data
{
	unsigned int key;
	char value[30];
	
}ListType, *pListType;

int print_set(ListType lt[],unsigned int n);
int hs_sift(ListType r[],int k,int n);
int getNTopValue(ListType lt[],int n);//Sub HeapSort - unprogramed.
int HeapSort(ListType r[], int n);

// heap sort in sequent order 
int hs_sift(ListType r[],int k,int n)
{
   	int i=k,j=2*i+1,finished= FALSE;
   	unsigned int x=r[k].key;
   	
   	ListType t = r[k];
  	while((j<n)&&(!finished))
  	{
  		//find the smaller child from the left and right children,
  		//find the bigger one,if in inverted sequent order
  		if ((j<n-1) && (r[j].key > r[j+1].key))     
			j++;
			
  		if (x <= r[j].key)
			finished= TRUE;
 		else 
 		{
 			r[i]= r[j];
 			i= j;
 			j= 2*i+1;
 		}
 	}
 	
 	r[i]= t;
 	
 	return 0;
}



// This algs needs one place;
int HeapSort(ListType r[], int n)
{
	int i;
	
	//build the heap
	//loop every unleaf node,and compare it with its children,grand...children.
	for(i=(n-1)/2; i >= 0; i--)
		hs_sift(r, i, n);
	
	//printf("after build deap!\n");
	//print_set(r,n);
	
	//delete the root node from heap,then rebuild the heap,until there no node left
	for(i=n-1; i > 0; i--)
	{
		//replace the root node using the last node,then resort it
		ListType t;
		t   = r[0];
		r[0]= r[i];
		r[i]= t;

		hs_sift(r, 0, i);
	}
	
	//printf("after deap sort!\n");
	//print_set(r,n);
	return 0;
}


// print the result in before sorting and after sorting 
int print_set(ListType lt[],unsigned int n)
{
	printf("=====================================\n");
	printf("start print....\n");
	int i=0;
	while(i < n )
	{
		printf("%u\n", lt[i].key);
		i++;
	}

	printf("print end ....\n");
	printf("=====================================\n");
	
	return 0;
}


int main(void)
{
	ListType big_set[NUMMAX+1];
	int i;
	
	//init the rander with different seeds;
	srand(time(NULL));
	
	for( i=0; i < NUMMAX; i++)
	{
		ListType t;
		memset(&t,'\0', sizeof(ListType));
		t.key=rand()%(NUMMAX); 
		//t.key=(unsigned int)(10-i);
		strcpy(t.value, "");
		
		big_set[i]= t;
	}
	
	printf("before sorting,the set is :\n");
	print_set(big_set, NUMMAX);
	
	HeapSort(big_set, NUMMAX);
	
	printf("after sorting, the set is :\n");
	print_set(big_set, NUMMAX);
	
	return 0;
}

     【?】

         1> 顺序用大根堆,逆序用小根堆,思考下为什么??

三、三围分析以及证明

时间复杂度

    建堆需要的时间:O(n)

    证明:设高度为h,则节点数总和最多为:2^h-1,某一层H最多的节点为:2^(H-1) 从根节点到最后一个非叶子节点,其最坏复杂度为它到最外层的路径长度,即假使每个节点均需要交换。另外这里的单位,是占用时间最主要的一个单位的交换所需时间。

    T(n) =1*(h-1) + 2*(h-2) + 4*(h-3) + ... + 2^(h-3)*(h-(h-2)) + 2^(h-3)*(h-(h-1))

    =h(1+2+4+...+ 2^(h-1)) - (1 + 2*2 +4*3 + 8*4 + 16*5 + .. + 2^(h-3) *(h-2) + 2^(h-2)*(h-1) )

   前面是个等比数列 其求和公式为:(a1-an*q)/(1-q) (q≠1)

   而后面这部分的通项公式为: 2^(n-1) * n,

   设这部分记为Sn,我们需要找出前n项和的规律来。

     我们对Sn*2 : 2*1 + 4*2 + 8*3 + 16*4 + ... + 2^(h-2) *(h-2) + 2^(h-1)*(h-1)

     然后 Sn*2 -Sn = Sn = 2^(h-1)*(h-1) - (1 + 2 + 4 + 8 + 16 + ... + 2^(h-2) )    ----后面这个又是一个等比数列,代入公式求解

     继续化简Sn:

                  Sn = 2^(h-1)*(h-1)  - 2^(h-1) = 2^(h-1)*(h-2)

     带入Sn,化简T(n):

                   T(n) = 2^h * h -  2^(h-1)*(h-2)

                          = 2^(h-1) * (h+2)

                          = 2^h * (h+2)/2

                           n * log(2,n)/2

     忽略常数项即:log(2,n) * n

        

    取堆顶元素并调整需要的时间:O(nlog(2,n))

     n个元素需要重复n次这样的动作,而每次这样的需要的时间与此时数的高度相关,粗略计算时可以认为高度不变

    这样时间复杂度为: n * log(2,n)

最坏情况:同平均复杂度

最好情况:O(1)

空间复杂度

    O(1),用来交换的临时空间。很容易证明:建堆过程不需要额外的空间,之需要交换用的一个单位的临时空间;取堆顶元素,并调整的过程,仔细观察过程并未真正把它取出来,而是跟堆尾元素交换,并调整堆尾下标即可。

稳定性

    把稳定性是由于其下标规则跟父子关系不一致导致的,父子比较并交换的情况下可以保证其稳定性,但是相同关键字的2个元素有可能并不是同一个父节点,因此并保证不了其稳定性。

【?】

        1> 解释下稳定性,以前也不明白,担心其他人也有不明白的。所谓稳定性是指如果2个元素的关键字相同,排序过程是否会打乱其顺序。这对于基于多个关键字排序是很有用的,如果不稳定,就不能直接应用于多个关键字的排序。自己想想为什么??

        2> 整个排序过程中,主要时间消耗在交换上,因此时间复杂度体现的是单位交换时间的次数。

       

四、算法直接应用   

    对较大的序列排序,时间复杂度同快速排序、归并排序,特长是使用很少空间,适合相对有序的序列排序。一般情况下,快速排序更好些,c语言的qsort库函数使用的就是快速排序。

    

五、算法原理应用 

    优先级队列:与普通队列的先进先出不同,这种队列插入和删除时取决于元素的优先级 ,这是一种非常有用的队列。操作系统就是使用这样一种数据结构来表示一组任务。而用堆实现同有序序列和无序序列相比,在插入和删除(或者叫提取)的效率上比较折中。

   优先队列有3个常用的函数:

   (1) 取最大(小)优先级的元素,其时间复杂度为 O(1)

   (2) 插入新的元素,过程相当于插入堆尾,然后进行堆调整。只是这里不需要重新做一遍建堆过程,而是从新插入的元素沿着它的父节点一直到根节点这 log(n+1)个节点。因此其时间复杂度为:

      T(n) = log(2,n+1) + (log(2,n+1)-1) + ... +2 + 1 

             = (1+log(2,n+1))* log(2,n+1)/2

             = (log(2,n+1)+1/2)^2 / 2 -1/8

              log(2,n+1)^2 /2  

     粗略计算的可以认为是:log(2,n)     

   (3)出去最大元素,并调整,这个时间复杂度就是 log(2,n)

六、举例 

1.  请给出一个时间为O(nlgk),用来将k个已排序链表合并为一个排序链表的算法。此处的n为所有输入链表中元素的总数。

 【解析】使用排序的归并方法的话,所用时间为: 2kn + (2kn+n) +...+((k-1)n+n)

编程思路:使用最小堆,有个朋友写得非常详细,就不重复了。在这里我们又一次体会到在动态的序列中堆的优势来。

假设k个链表都是非降序排列的。

(1)取k个元素建立最小堆,这k个元素分别是k个链表的第一个元素。建堆的时间复杂度O(k)。

(2)堆顶元素就是k个链表中最小的那个元素,取出它。时间复杂度O(1)。--只要在最小堆中保证每个序列都有一个元素,就可以保证最小堆取出来的一定是当前的最小值。

(3)若堆顶元素所在链表不为空,则取下一个元素放到堆顶位置,这可能破坏了最大堆性质,所以进行堆调整。堆调整时间复杂度O(lgk)。若为空,则此子链表已经被合并完毕,则删除最小堆的堆顶元素,此时最小堆的heapSize减小了1 。删除指定元素时间复杂度O(lgk)。

(4)重复步骤(2)~(3)n-k次。总的时间复杂度是O(k)+O(nlgk)即O(nlgk)。 http://hi.baidu.com/tuangougou/item/872f45d42f69ad3838f6f754

2. 一个文件中包含了1亿个随机整数,如何快速的找到最大(小)的100万个数字?(时间复杂度:O(n lg k))

分析:为了更深刻理解这个题目,我们使用3种方法解决这个问题:       

1)     直接法:先找出最大,然后第二大,半个选择排序,复杂度:10000* n      

2)     堆排序原理法: 先构建最大堆:时间复杂度为n ,然后进行10000次取堆顶元素,并调整 10000*log(2,n) 那么               总时间为 n+10000*log(2,n)=n+10000*10000 = 2n       

3)     快速排序原理法:快速排序的方法是每次用第1个元素对序列一分为2 ,然后分别对2部分进行递归快速查                    找。 如果只取前100w个,则如果判断前面部分总数大于100w个,则后面那部分不用理会。这样递归,如果                前面那部分总数最大接近100w的时候,就可以进行一次全排序。那么时间复杂度为:                log(2,100w)*100w + n+ n/2 + n/4 …+ n/10000  接近2n。


                
  • 7
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值