• 堆排序


     堆排序是由1991年的计算机先驱奖获得者、斯坦福大学计算机科学系教授罗伯特.弗洛伊德(Robert W.Floyd)和威廉姆斯(J.Williams)在1964年共同发明了的一种排序算法( Heap Sort );

            堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。

    基本介绍

    先直观感受一下,下面就是一个堆:

    20 17 8 7 16 3

    什么??上面不就一个数组吗……?!

    没错,(二叉)堆数据结构是一种数组对象。

    不过,让我们用另外一种方式来看这个数组:

    对于表示堆的数组arr[0…n-1],我们以arr[0]为根,给定某个节点下标i,令其父节点和左右后代节点的下标为:

    parent(i) = (i-1)/2;

    left(i) = 2*i+1;

    right(i) = 2*i+2;

    (具体实现时,可用移位来实现乘以2和除以2)

    于是,它可以看作一棵完全二叉树:(参考根据数组创建完全二叉树)

    这里写图片描述

    可是,这也只是一棵完全二叉树,有啥特别之处呢?

    特点就是:除根节点以外的每个节点i,都有arr[ parent(i) ] >= arr[i]。

    堆分为最大堆和最小堆,上面就是最大堆,最小堆的特点则是:除根节点以外的每个节点i,都有arr[ parent(i) ] <= arr[i]。

    堆排序一般使用最大堆,最大堆中的最大元素位于根节点。

    因为具有n个元素的堆是基于一颗完全二叉树的,所以其高度为O(log n)。

    堆的基本性质:

    算法分析

            其实这种算法看起来挺复杂,但是如果真正理解了就会感觉非常简单的;

            基本思想:把待排序的元素按照大小在二叉树位置上排列,排序好的元素要满足:父节点的元素要大于等于其子节点;这个过程叫做堆化过程,如果根节点存放的是最大的数,则叫做大根堆;如果是最小的数,自然就叫做小根堆了。根据这个特性(大根堆根最大,小根堆根最小),就可以把根节点拿出来,然后再堆化下,再把根节点拿出来,,,,循环到最后一个节点,就排序好了。

            基本步骤:堆化(保持堆的性质)----->创建初始堆(i = nLength/2-1;i>=0调整所有父亲节点,调用堆化过程)

                        ----->堆排序(a[0]和数组最后一个元素a[i]互换位置------>重新调整堆)(从i=n-1到i>0)

            其实整个排序主要核心就是堆化过程,堆化过程一般是用父节点和他的孩子节点进行比较,取最大的孩子节点和其进行交换;但是要注意这应该是个逆序的,先排序好子树的顺序,然后再一步步往上,到排序根节点上。然后又相反(因为根节点也可能是很小的)的,从根节点往子树上排序。最后才能把所有元素排序好;

    具体步骤:

    可以参考:http://blog.csdn.net/jiange_zh/article/details/50700481,里面是根据算法导论来的

    时间复杂度

    这里我们首先要注意堆的高度的概念:堆可以被看成是一棵树,结点在堆中的高度可以被定义为从本结点到叶子结点的最长简单下降路径上边的数目;定义堆的高度为树根的高度

    我们知道二叉树有一个性质:1.拥有k层的二叉树在第k层最多有2^(k-1)个结点

                 2.拥有k层的二叉树最多有2^(k)-1个结点,最少有2^(k-1)+1个结点

    而我们形容堆的时候常形容为堆的高度,和树的层数概念和值都是不同的,堆的高度为相应二叉树的层次减一,所以想用二叉树的性质求解堆的问题时,在求高度为k的堆的相应问题时,带入的应该是k+1,才可以直接使用二叉树的性质进行计算。

    1.关于堆排序建堆时间复杂度的证明:

     假设高度为k,则从倒数第二层右边的节点开始(最后一个父亲节点),这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;

    首先,对于高度为h的完全二叉树,其第i层的元素个数为2^(i-1),对于堆的每一层,调整的深度都不一样,每层的元素的调整深度小于等于h-i,假设每层调整的深度是h-i,欲构建的堆是个完全二叉树,那么对于每层来说:

    最后一层不用调整;

    倒数第二层的消耗是:2^(h-1)*1;

    倒数第三层的消耗是:2^(h-2)*2;

    。。。。。。

    第一层的消耗是:2^(h-h)*(h-1);

    加起来总消耗是:S=2^(h-1)*1+2^(h-2)*2+。。。+h;

            2S=2^h*1+2^(h-1)*2+。。。+2*h;

            S=2^h+2^(h-1)+2^(h-2)+。。。+2^1-h;

            S=2^h+2^(h-1)+2^(h-2)+。。。+2^1+2^0-h-1;

            S=2^(h+1)-2-h;

            h=logn;

    代入得:S=2*n-2-logn;

    堆排序的建堆过程是O(n)的

    2.更改堆元素后重建堆时间:O(nlogn)

      推算过程:

           1、循环  n -1 次,每次都是从根节点往下循环查找,所以每一次时间是 logn,总时间:logn(n-1) = nlogn  - logn ;

        两段代码选时间复杂度比较大的那个作为最终的时间复杂度。

           综上所述:堆排序的时间复杂度为:O(nlogn)

    堆排序代码如下:

    #include<stdio.h>
    
    #define LEFT 2*nRootID +1
    #define RIGHT 2*nRootID+2
    void Adjust(int arr[],int nLength,int nRootID)
    {
    	while(1)//别忘了循环
    	{
    		//两个孩子,因为是完全二叉树,所以右孩子存在则左孩子一定存在
    		if(RIGHT < nLength)
    		{
    			//比较两个孩子大小
    			if(arr[LEFT] > arr[RIGHT])
    			{
    				//大的和父亲比
    				if(arr[LEFT] > arr[nRootID])
    				{
    					arr[LEFT]  =arr[LEFT]^ arr[nRootID];
    					arr[nRootID]  =arr[LEFT]^ arr[nRootID];
    					arr[LEFT]  =arr[LEFT]^ arr[nRootID];
    
    					nRootID = LEFT;//如果调整后的节点本身又是一个父亲节点,
    					continue;//则接着调整
    				}
    				break;//如果当前节点本身最大,结束对当前节点的调整操作
    			}
    			else
    			{
    				if(arr[RIGHT] > arr[nRootID])
    				{
    					arr[RIGHT] = arr[RIGHT]^arr[nRootID];
    					arr[nRootID] = arr[RIGHT]^arr[nRootID];
    					arr[RIGHT] = arr[RIGHT]^arr[nRootID];
    
    					nRootID = RIGHT;
    					continue;
    				}
    				break;
    			}
    		}
    		//一个孩子
    		else if(LEFT < nLength)
    		{
    			if(arr[LEFT] > arr[nRootID])
    			{
    				arr[LEFT]  =arr[LEFT]^ arr[nRootID];
    				arr[nRootID]  =arr[LEFT]^ arr[nRootID];
    				arr[LEFT]  =arr[LEFT]^ arr[nRootID];
    
    				nRootID = LEFT;
    				continue;
    			}
    			break;
    		}
    		//没有孩子
    		else
    		{
    			break;
    		}
    	}
    }
    void Adjust2(int arr[],int nLength,int nRootID)//优化后的堆化过程版本
    {
    	int MAX;
    	for(MAX = LEFT;MAX < nLength; MAX = LEFT )
    	{
    		//如果有两个孩子
    		if(RIGHT < nLength)
    		{
    			if(arr[MAX] < arr[RIGHT])
    			{
    				MAX = RIGHT;
    			}
    		}
    
    		//大的和父亲比
    		if(arr[MAX] > arr[nRootID])
    		{
    			arr[MAX] = arr[MAX]^arr[nRootID];
    			arr[nRootID] = arr[MAX]^arr[nRootID];
    			arr[MAX] = arr[MAX]^arr[nRootID];
    
    			nRootID = MAX;
    		}
    		else
    		{
    			break;
    		}
    	}
    }
    
    
    
    void HeapSort(int arr[],int nLength)
    {
    	if(arr == NULL || nLength <=0)return;
    
    	//建堆
    	int i;
    	for(i = nLength/2-1;i>=0;i--)
    	{
    		//调整各个父亲节点
    		Adjust2(arr,nLength,i);//从倒数第二层从下向上调整
    	}
    
    	//排序
    	for(i = nLength-1;i>0;i--)
    	{
    		//堆顶和最后位置交换
    		arr[0] = arr[0] ^ arr[i];
    		arr[i] = arr[0] ^ arr[i];
    		arr[0] = arr[0] ^ arr[i];
    
    		//重新调整堆顶
    		Adjust2(arr,i,0);//i:传入的长度在减小,当前堆顶已经排好后不必参与下一次排序  0:只对堆顶调整即可,此时是从上向下调整
    } }//红色标注部分决定了是从下往上调整还是从上向下调整 int main() { int arr[] = {10,4,7,9,20,3,28,219,523}; HeapSort(arr,sizeof(arr)/sizeof(arr[0])); int i; for(i = 0;i<sizeof(arr)/sizeof(arr[0]);i++) { printf("%d ",arr[i]); } return 0; }

      

     参考资料:http://blog.csdn.net/u011663071/article/details/44065813

          http://blog.csdn.net/yuzhihui_no1/article/details/44258297

  • 相关阅读:
    软件测试入门知识
    QTP小应用一则
    频分时分波分码分
    解析UML9种图的作用
    OSI七层模型
    暑期实习心得
    0724工作小结 SQL查库是重点
    0723脚本存储过程的学习
    0722工作日志
    工作之余回味了曾经的写过的小说
  • 原文地址:https://www.cnblogs.com/curo0119/p/8419342.html
Copyright © 2020-2023  润新知