排序 —— 堆排序

一、算法介绍

  • 堆:堆是一种数据结构,可以把堆看成是一棵完全二叉树,这棵完全二叉树满足:任何一个非叶结点的值都不大于(或不小于)其左右孩子结点的值。若父亲大孩子小,则这样的堆称为大顶堆;若父亲小孩子大,则这样的堆称为小顶堆。
  • 堆化:将当前结点(假设为a)的值与其孩子结点进行比较,如果存在大于 a 值的孩子结点,则从中选出最大的一个与a交换,当 a 来到下一层的时候重复上述过程,直到 a 的孩子结点的值都小于 a 的值为止。

二、算法思想

堆排序的执行过程描述(以大顶堆为例)如下:

  1. 建堆: 从无序序列所确定的完全二叉树的第一个非叶子结点开始,从右往左,由下往上,对每个结点进行堆化。
  2. 排序: 建堆结束之后,堆顶元素就是最大元素,我们将其和最后一个元素进行交换,那最大元素就放到了下标为 n 的位置,此时,无序序列的关键字减少1个,有序序列的关键字增加1个。然后,我们再对前面 n−1 个元素进行堆化,再将堆顶元素放到下标为 n−1 的位置。重复这个过程,直到堆中剩余一个元素,排序也就完成了。

三、算法过程

现在要完成6个整数 { 6,5, 14,7, 8, 1 } 的大顶堆排序

(1)建堆

  1. 调整14,14>1,满足堆定义,不需要调整。
  2. 调整5,5<7,5<8,不满足堆定义,故交换8和5,交换后5成为了叶子结点,故结束调整。
  3. 调整6,6<8,6<14,不满足堆定义,故交换6和14。继续调整6,6>1,满足堆定义,不需要调整
    在这里插入图片描述

(2)排序1

  1. 将堆顶14与最后一个元素1进行交换,无序序列的关键字减少1个,有序序列的关键字增加1个。
  2. 调整1,1<8,1<6,交换1和8。继续调整1,1>5,1>7,交换1和7,交换后1成为叶子结点,故结束调整
    在这里插入图片描述

(3)排序2

在这里插入图片描述

(4)排序3

在这里插入图片描述

(5)排序4

在这里插入图片描述

(6)排序5

在这里插入图片描述


四、堆排序的插入删除操作

(1)插入结点

需要在插入结点后保持堆的性质,因此需要先将要插入的结点x放在最底层的最右边,插入后满足完全二叉树的特点;然后把 x 依次向上调整到合适位置以满足堆的性质。

(2)删除结点

当删除堆的一个结点时,原来的位置就会出现一个空缺,填充这个空缺的方法就是,把最底层最右边的叶子的值赋给该空缺并下调到合适位置,最后把该叶子删除。


五、实现

void translate(int R[],int low,int high)	//	在数组R[low]到R[high]的范围内对在位置low的结点进行调整
{
	int i=low;	//	父结点
	int j=i*2;	//	左孩子
	int temp=R[i];	//	记录父结点
	while(j<=high)
	{
		if(j<high && R[j]<R[j+1])	//	若右孩子比较大,则把j指向右孩子
			j++;
		if(temp<R[j])
		{
			R[i]=R[j];	//	将R[j]调整到父结点的位置
			i=j;	//	修改i和j的值,以便继续向下调整
			j=2*i;
		}
		else
			break;	//	父结点较大,调整结束
	}
	R[i]=temp;	//	将被调整结点的值放入最终位置
}
void heapSort(int R[],int n)
{
	int i;
	int temp;
	for(i=n/2;i>=1;i--)	//	建立初始堆
	{
		translate(R,i,n);
	}
	for(i=n;i>=2;i--)
	{
		temp=R[i];	//	无序序列的堆顶和最后一个元素交换
		R[i]=R[1];
		R[1]=R[i];
		translate(R,1,i-1);
	}
}

六、算法性能分析

(1)时间复杂度分析

对于函数 translate(),显然 j 走了一条从当前结点到叶子结点的路径,完全二叉树的高度为 floor(log(n+1)),即对每一个结点调整的时间复杂度为 O(log n)

对于函数 heapSort(),基本操作总次数应该是两个并列的 for 循环中的基本操作次数之和,第一个循环的基本操作次数为 O(log n)*(n/2),第二个循环的基本操作次数为 O(log n)*(n-1),因此整个算法的基本操作次数为 O(log n)*(n/2) + O(log n)*(n-1),故其平均时间复杂度、最坏时间复杂度、最好时间复杂度均为 O(nlog n)

堆排序和快速排序的最好时间复杂度都是 O(nlog n),那么两者之间有什么不同呢?

  1. 10w 数据量两种排序速度基本相当,但是堆排序交换次数明显多于快速排序;10w+数据,随着数据量的增加快速排序效率要高的多,数据交换次数快速排序相比堆排序少的多。

  2. 实际应用中,堆排序的时间复杂度要比快速排序稳定,快速排序的最差的时间复杂度是O(n^2),平均时间复杂度是O(nlog n)。堆排序的时间复杂度稳定在O(nlog n)。但是从综合性能来看,快速排序性能更好。

(2)空间复杂度分析

空间复杂度为 O(1)

(3)稳定性

我们知道堆的结构是节点 i 的孩子为 2i 和 2i+1 节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但建堆的过程是从右往左,从下往上选择非叶子结点,当非叶子结点为n/2-1,n/2-2,…这些个父节点选择元素时,就会破坏稳定性。有可能第 n/2 个父节点交换先把后面一个元素交换过去了,而第 n/2-1 个父节点没有把后面一个相同的元素交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

猜你喜欢

转载自blog.csdn.net/starter_____/article/details/94193977