算法设计与分析——堆排序

目录

  • 前言
  • 一、算法思想分析
  • 二、算法效率分析
  • 三、算法代码
    • C语言代码
  • 后记

前言

一个高效的排序算法,对整个计算机领域的贡献都是极大的。在我看来,堆排序算法就是这样一种高效的排序算法,它本身的设计就是非常巧妙的。

一、算法思想分析

要理解堆排序,首先就得理解,什么是堆?在《算法设计与分析基础》中有如下定义:
算法设计与分析——堆排序_第1张图片
简而言之,所谓堆,就是一棵二叉树(如果大家不理解二叉树的概念的,可参考百度百科——二叉树,等我以后写了二叉树分析的博客再附上我自己的,嘿嘿。),一棵父节点元素一定大于等于子节点元素完全二叉树(以上不是水字数,只是附上我的理解罢了)。
算法设计与分析——堆排序_第2张图片
如上图,看第二个,首先其根本不是一棵完全二叉树,那么也就不可能称作是堆了。看第三个,包含元素5的节点,其子节点中,存在一个包含元素6的节点,显然不满足堆的特性,也不是堆。综上分析,只有第一棵树可以称作是堆
算法设计与分析——堆排序_第3张图片
而堆排序呢,正是利用了堆的这些特性,故而称作堆排序(怎么听起来有点拗口呢?不管了,就是这个意思就对了)。对于二叉树,我们通常用数组来进行存储,稍微特殊一点的是,我们存储位置的起始下标是1,不是0。那么,这个时候我们就可以得出父节点和子节点的数组下标之间的关系了。
算法设计与分析——堆排序_第4张图片
那么,给出我们一个数组,我们如何来构造一个堆呢?我们正是利用的以上父子节点之间下标的关系来构造一个堆。
在这里,我们自底向上来构造堆(自顶向下的方法其实差不多,大家有兴趣的可以研究研究)。

  • 1.首先找到最后一个父(母)节点,由上我们可知,最后一个父节点的下标是数组长度除2后向取整
  • 2.找到一个父节点后,将该父节点与其两个子节点进行比较,左孩子和右孩子的下标我们同样可以找到,在两次比较中,如果子节点的元素比父节点的元素大,那么我们将两个节点的元素交换。两次比较完之后呢?这个时候我们就结束了么?如果没有元素交换,那么本次比较就结束了,但如果发生了元素交换,那么就没有结束。我们要完成一次堆的构造,就必须保证这个二叉树满足堆的所有特性,当此时并没有完全满足。因为我们上述操作不过是将父元素与其子元素比较,但我们并不能保证当前被交换的子节点它就不是一个父节点(想象一下,一个节点是不是可能既是子节点又是父节点?),那么根据堆的特性,我们就要将此时被交换的子节点看作是一个父节点,来重复我们前面的操作(也就是继续和其子节点比较),直到这个节点他大于他的所有子节点(亦或是没有子节点了),那么本次交换就到此结束了(这里讲解篇幅略长,如果实在看不太懂,可以先看下面的算法实现,然后回来看)。
  • 3.这个时候,我们刚才找到的那个父节点的所有交换就已经完成了,我们就要开始去走它的上一个节点(自底向上就是这么来的),也就是下标减一(说白了,我们就是在遍历所有的父节点并将这些父节点和他们的子节点比较大小),这里的父节点我们要指明一下,这里的父节点是从n/2向下取整开始一个个递减所有父节点。
  • 4.重复上述第二步和第三步操作,直至我们遍历到第一个节点并比较完毕。
  • 5.第四步完成之后,我们就完成了一次堆构造了,此时我们的根节点一定是一个元素值最大的节点,并且当前一定是一个堆。那么这个时候,关键的一步来了。将根节点元素与当前数组最后一个元素交换,当前数组的最后一个元素非常之关键,那么我们交换后,当前数组的最后一个元素就是当前数组中最大的元素了。
  • 6.将当前数组的长度减一,从第一步开始继续重复操作。直至数组长度为1,算法结束。

堆排序算法思想的讲解稍微有些复杂,但我这里尽量详细一些,但就是又有些显得冗余了好像,具体的还是推荐大家看代码。
下图是一次堆构造的过程,可供大家参考。
算法设计与分析——堆排序_第5张图片

二、算法效率分析

无论是最差还是最优的情况,堆排序的时间效率都属于O(nlogn)。
分析如下:
算法设计与分析——堆排序_第6张图片

三、算法代码

C语言代码

这里的C语言代码,我仍然采用随机数的生成来生成测试数据。

/*堆排序算法——自底向上*/
#include
#include
/*利用全局数组 避免传参*/
#define MAXN 1000000
int arr[MAXN];

/*交换算法*/
void swap(int i,int j) {
     
	int t=arr[i];
	arr[i]=arr[j];
	arr[j]=t;
}

/*构造堆算法*/
void HeapBottomUp(int n) {
     
	/*顾名思义 自底向上构造堆算法*/
	for(int i=n/2; i>=1; i--) {
     
		int k=i;
		int v=arr[k];
		bool heap=false;
		while(!heap&&2*k<=n) {
     
			/*先取左子节点*/
			int j=2*k;
			/*左右子节点的比较 大的获胜*/
			if(j<n) if(arr[j]<arr[j+1]) j=j+1;
			/*父子节点的比较*/
			if(v>=arr[j]) heap=true;
			/*这一步很关键 如果子节点元素大于父节点元素 则需要向下开始交换 直到当前节点元素大于子节点元素 */
			else {
     
				arr[k]=arr[j];
				k=j;
			}
		}
		/* 为保证向下交换的结果正确 将当前arr[k]的值赋予初值v 要知道 当前的k可能已经改变 */
		arr[k]= v;
	}
}

/*排序总算法*/
void HeapSort(int n) {
     
	/*在每一次构造完堆后 将第一个元素与最后一个元素交换 并且数组长度减一 然后进行下一次构造堆 直到数组长度为一 则停止构造堆*/
	for(int i=n; i>=2; i--) {
     
		HeapBottomUp(i);
		/*交换第一个元素和当前数组的最后一个元素*/
		swap(1,i);
	}
}

int main() {
     
	int n;
	//scanf("%d",&n);
	/*利用随机数生成数组*/ 
	n=rand() % 200;  // 200内的随机长度 
	/*注意 数组从1开始用 便于思维计算*/
	for(int i=1; i<=n; i++) {
     
		// scanf("%d",&arr[i]);
		arr[i]= rand() % 2000;
	}
	/*调用堆排序算法 传参:数组长度*/
	HeapSort(n);
	/*输出结果*/
	printf("随机长度:n=%d\n",n);
	for(int i=1; i<=n; i++) {
     
		if(i!=n) printf("%d -> ",arr[i]);
		else printf("%d",arr[i]);
	}
}

代码运行示例如下:
代码运行示例

后记

堆排序算法,是一个非常优秀的排序算法,其算法相对稳定,且无需额外的空间来完成。并且其思想是利用了二叉树,对于学习二叉树也是一个不错的帮助。正如上面所说,堆排序的时间效率和合并排序的时间效率属于同一类。而且,与后者不同,堆排序是在位的,也就是说,它并不需要任何额外的存储空间。针对随机文件的计时实验指出,堆排序比快速排序运行得慢,但和合并排序相比还是有竞争力的。
以上就是堆排序算法的总结了,如果有任何疑问或建议,欢迎大家私信或留言。

你可能感兴趣的:(算法设计与分析,堆排序,c语言,算法,排序算法,c算法)