堆排序(详解:向上调整算法&向下调整算法)

目录

一、前言

二、分析

2.1 向上调整算法

2.2 向下调整算法

三、代码实现

四、改进

五、分析时间复杂度

5.1 向上调整算法

5.2 向下调整算法


一、前言

堆排序,顾名思义利用堆的结构特性对一组数据进行排序。

那么什么是堆呢?

二叉树的实现分为两种,顺序结构实现二叉树或者是链式结构实现二叉树。使用顺序结构也就是数组来储存一定的数据实现二叉树结构就叫堆。所以堆就是二叉树,而且是一种特殊的二叉树,在具备普通二叉树性质的同时,还得具备完全二叉树的特点。

堆的底层结构是数组。

堆排序分为两种,大根堆和小根堆:

①大根堆(大堆):每个结点的值都大于或等于其左右孩子结点的值

②小根堆(小堆):每个结点的值都小于或等于其左右孩子结点的值

注意!!!并不要求结点的左右孩子的值的大小关系

[本文章统一以小根堆为背景来介绍]

我们知道在堆的基本操作中,出堆一般都是按照一定的顺序。大堆出堆是升序,小堆出堆是降序。那么借助这一特点,我们就可以利用出堆的这个思想来给一组数据进行排序。


二、分析

首先,因为堆的底层结构是数组,那么待排序列也可以存在数组里面,方便后续操作。

但是不能一直都原封不动的在数组里面放着,毕竟我们是要借助堆的结构特性去排序的。因此啊这个时候,我们就要先将这个数组里面数据的排放结构进行调整(如下图),使其符合一个堆的结构。

这里我们用到的是向上调整算法建堆的。

2.1 向上调整算法

那么啥是向上调整算法呢?

基本思路:将元素插入到堆的末尾。插入之后如果堆的结构特性发生改变(即不满足大根堆or小根堆),那么就找到该结点的父节点,交换孩子结点和父亲结点。以此类推,顺着往上的来进行调整堆的结构。[看不懂的可以根据上图来理解]

void AdjustUp(HPDataType* arr, int child)
{
	int parent = (child - 1) / 2; //先由孩子找父母

	while (child > 0)//child只需要走到根节点的位置,因为根节点没有父节点不需要交换,也就不用在向上调整了
	{
		//这是建小堆
		if (arr[child] < arr[parent])
		{
			Swap(&arr[parent], &arr[child]);

			child = parent;//重复步骤
			parent = (child - 1) / 2;
		}
		else
		{
			break;//直至满足堆的性质
		}
	}
}


void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

刚刚在前言中,我们有提到是利用出堆的思想(出堆是按照顺序的)去进行最终的排序。因为我们建立的是小堆,所以出堆的时候,是降序。

那怎么出堆呢?

出堆就是将堆顶元素和末尾元素,进行交换。然后再删除末尾元素。

因为堆顶永远都是这个堆所包含的数据中最大or最小的,因此出堆才按照一定的顺序。

就拿我们建的这个小堆来说,堆顶是最小的元素2。将2出堆后,根据堆的结构特性,接下来的堆顶一定是次小的3,依次类推,最终出堆的顺序就是2,3,5,6,7。

但是请注意哈!我们这里只是借用出堆的思想去排序,而不是真正的出堆。一开始待排序列在数组中存储,那么最终排好的序列也应该在数组中存储,即7,6,5,3,2。

堆顶元素和末尾元素交换的本质就是,下标进行了变换。堆顶2和末尾6交换,堆顶的2这个时候就存储在数组的末尾位置arr[4]。

但是啊,我们下一次交换时,还是要用到末尾位置的,难道继续用arr[4]进行交换吗?

虽然说我们排序的时候只是借用出堆思想,而不真正出堆,可是如果又把末尾位置的2给调到堆顶arr[0]上,显然就是不合理的。

所以真正的做法就是每次交换完,就将下标-1,假装之前的末尾元素已经没有了。那么下一次进行交换的时候末尾元素就会变成arr[3]

可以发现,每次交换完,也需要调整堆的结构。

上面建堆的时候,是用向上调整算法。这里调整堆,我们用向下调整算法。

2.2 向下调整算法

注意!!!向下调整算法有一个前提,左右子树必须是一个堆,才能进行调整

基本思路:给一个结点(父节点),找到该结点的孩子,比较两个结点的大小,判断时候需要交换,依次往下,直至符合堆结构特性。如果给的是堆顶元素,那么就从堆顶元素向下调整。

void AdjustDown(HPDataType* arr, int parent, int n)
{//向下调整算法,要传一个父节点,从这个父节点开始向下
	int child = parent * 2 + 1;//找到左孩子

	while (child < n)//理解这一步是很重要的,当孩子到叶子结点时就可以跳出循环了
	{
		//这是建小堆
		//找左右孩子中最小的
		if (child + 1 < n && arr[child] > arr[child + 1])
		{
			child++;
		}
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);

			parent = child;//重复步骤
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

之前的第一步调整细节如下:

从堆顶arr[0]开始向下调整,可知它的孩子是arr[1]和arr[2]。

先比对arr[1]和arr[2]的大小,选出最小的(因为这里建的是小根堆)和arr[0]进行比较交换

交换完成后,发现堆已经满足特性,故不需要再向下调整。否则重复上述步骤,直至调整完成。

到此,堆排序的逻辑思路已讲清楚,接下来就是代码实现。


三、代码实现

堆排序代码:

//堆排序(利用堆的结构特点,对数据进行排序)
// 想要升序就建大堆,想要降序就建小堆
// 这里是降序
void HeapSort(int* arr, int n)//参数要传一个数组,还有数组里面的元素个数
{
	//向上调整算法建堆
	for (int i = 0; i < n; i++)
	{
		AdjustUp(arr, i);
	}
	//上面步骤结束之后初步建立了一个符合二叉树结构中的堆结构,但是不一定排好了顺序
	
	//利用出堆的思想(因为出堆是按照一定顺序的)
	//循环将堆顶元素跟最后位置的元素进行交换,最后一个元素会变化,下标每次都会减少1
	int end = n - 1;//end为末尾元素
	while (end > 0)
	{
		Swap(&arr[0], &arr[end]);//这里的交换是针对数组而言,调整顺序,真正进行排序
		AdjustDown(arr, 0, end);//从根节点开始向下调整堆结构
		end--;
	}
	
}

测试堆排序的代码:

int main()
{

	//测试:堆排序
	int arr[] = { 17,20,10,13,19 };
	HeapSort(arr, 5);
	for (int i = 0; i < 5; i++) //打印排好的数组
	{
		printf("%d ", arr[i]);
	}
	printf("\n");

	return 0;
}


四、改进

由上面可知,我们建堆用的是向上调整算法,后续调整堆用的是向下调整算法。

这里是我为了既能介绍到向上调整算法又能介绍到向下调整算法,而特意做的安排。

那么在实际实现当中,为了提高效率,我们可以都使用向下调整算法。

如果利用向下调整算法建堆,代码如下:

//向下调整算法建堆    
for (int i = (n - 1 - 1) / 2; i > =0; --i)
{
	AdjustDown(arr, i, n);//这里的i就是父节点
}

图解析:     

结合向下算法建堆的代码和AdjustDown代码共同理解

那为什么向下调整算法能够提高效率呢?又为什么不采用向上调整算法呢?接下来,我们探究一下向下调整算法和向上调整算法的区别


五、分析时间复杂度

5.1 向上调整算法

向上调整,顾名思义是由下到上进行调整。

也就是每次我们插入一个新结点的时候,都要向上看看,是否符合的堆的结构,如果不符合,那就向上移动。

堆的第k层,最多有2^(k-1)个结点

分析向上调整算法的移动:越往下,结点越多,移动的层数也越多

第一层时满的情况下,有1个根结点,没有更往上的一层,不需要进行调整
第二层时满的情况下,有2个结点,最差往上调整1层
第三层时满的情况下,有4个结点,最差往上调整2层
…………    (最差指的是最差情况下)
第n层时满的情况下,有2^(n-1)个结点,最差往上调整n-1层

          每层移动步数 = 每层的结点数 * 每层往上移动的步数

将每层移动步数相加得到总移动步数,再进行数学代换,把得到的式子,简化一下,最终得到向上调整算法的时间复杂度:  O(n*log2 n)


5.2 向下调整算法

向下调整,顾名思义有上到下进行调整。

①出堆里应用:通常是从堆顶向下调整,使其符合堆结构。

②建堆里应用:给一个父节点,找到其孩子结点,向下调整。

分析向下调整算法的移动:越往下,结点越多,但移动的层数越少

第一层时满的情况下,有1个根结点,最差从根结点就得交换,往下n-1层
第二层时满的情况下,有2个结点,最差往下调整n-2层
第三层时满的情况下,有4个结点,最差往下调整n-3层
…………
第n层时满的情况下,有2^(n-1)个结点,到达叶子结点,无需往下调整

          每层移动步数 = 每层的结点数 * 每层往下移动的步数

将每层移动步数相加得到总移动步数,再进行数学代换,把得到的式子,简化一下,最终得到向下调整算法的时间复杂度:  O(n)


由此,我们发现向下调整算法的时间复杂度是小于向上调整算法的。

故使用向下调整算法效率会更高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值