数据结构:堆排序

堆排序

光听名字,你可能会以为这是用堆来排序的算法,实则不然。堆排序是利用堆的思想来进行排序的,那么是如何来进行排序的?

现在随便给出一组数组:

c 复制代码
int arr[8]={12,20,23,2,4,14,13,21};

我们之前实现堆的时候,知道堆分两种,一种是大堆,一种是小堆。如果我们要把这个数组arr建成一个大堆,该如何来进行排序?

建堆的算法我们分为两种,一种是向下排序算法,一种是向上排序算法,我们来实现一下。

向下排序算法

思路

向下排序算法中,因为我们要保证整个二叉树结构是一个大堆结构,所以我们先从最后一棵子树开始调整。

最后一个结点的父结点肯定就是最后一棵子树的根结点,比如这棵树,最后一个结点的位置是7,(7-1)/2=3,3就是最后一棵子树的根结点。

向下调整的函数Adjustdown我们之前写过,这里就直接给出来了。

c 复制代码
void Adjustdown(int* 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[parent] < arr[child])//调整为大堆结构
		{
			Swap(&arr[parent], &arr[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}

把最后一棵子树调整为大堆结构后,我们parent - - 就可以找到上一个子树的根结点,我们再接着调整这棵树为一个大堆结构:

调整完成后,parent再 - - 找上一棵子树,再继续调整:

调整完成后再parent - -,此时会发现parent=0,说明是最后一次调整了,调整的是整个树的结构:

调整完成后就会变成一个大堆:

代码实现

理清思路后,我们就可以来实现一下代码了。

我们要从最后一棵子树开始,遍历调整所有子树的结构,所以我们写个for循环。

假设数组大小为n,最后一个结点的位置就是n-1,根据堆的性质,我们就可以找到它父结点parent的位置了:(n-1-1)/2,每次调整子树结构完成后,parent都要 - -找上一棵子树,直到parent=0时,说明是调整整棵树,是最后一次调整,所以循环条件我们给一个parent大于等于=0,这里我们用 i 来表示父结点,用来和Adjustdown的parent区分开来:

我们来测试一下:

写成二叉树形式是这样的:

说明成功被排为了大堆结构,代码功能没问题。

向上排序算法

思路

向上排序算法的思路非常简单,可以类比为入堆操作,遍历一遍数组一个个入堆,然后每次入堆都调整保证是一个大堆结构。每次调整都调用函数Adjustup,我们之前实现过,这里就直接给出来了。

c 复制代码
void Adjustup(int* arr, int child)
{
	int parent = (child - 1) / 2;
	while (parent>=0)
	{
		if (arr[parent] < arr[child])//调整为大堆
		{
			Swap(&arr[parent], &arr[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}

代码实现

我们简单测试一下:

写成二叉树结构是这样的:

说明该代码功能没问题。

向下排序算法和向上排序算法的时间复杂度

那么这两个算法哪个更好呢?

观察一下,我们发现这两个算法都是一个循环,每次循环调用这个函数(时间复杂度为O(logn)),那这两个算法的时间复杂度都是O(nlogn)吗?下面我们用满二叉树来分析一下这两个算法。

向下排序算法的时间复杂度

这里有一个k层的满二叉树,向下排序算法是从第k-1层开始往前遍历子树的根结点的。

第k-1层有2k-2个结点,向下调整要移动1次。

第k-2层有2k-3个结点,向下调整要移动2次。

第k-3层有2k-4个结点,向下调整要移动3次。

...

第2层有21个结点,向下调整要移动k-2次。

第1层有20个结点,向下调整要移动k-1次。

可知向下排序移动的总步数为:每层结点个数*向下调整移动次数。

设T(k)为向下排序算法的总步数,则有:

两边同时乘2,可以得到:

2式减1式错位相减可以得到:

1可以写成20,用等比数列求和公式可以整理成:

根据二叉树的性质:2k-1=n,k=log2n+1,

k是层数,用n代换掉k可以得到: O(n)=n-log2n-1

所以向下调整算法的时间复杂度为O(n)。

向上排序算法的时间复杂度

每次调整都到第一层为止。

第一层有20个结点,向上调整0层。

第二层有21个结点,向上调整1层。

第三层有22个结点,向上调整2层。

...

第k-1层有2k-2个结点,向上调整k-2层。

第k层有2k-1个结点,向上调整k-1层。

易知向上排序算法移动的总步数:每层结点数*移动层数

设移动总步数为T(k),则有:

两边同时乘2可以得到:

2式减1式错位相减得:

-2和-2k写入括号里整理可得:

等比数列求和整理得:

根据二叉树的性质:2k-1=n,k=log2n+1,把k代换成n可得:

所以向上排序算法的时间复杂度为O(nlogn

向下排序算法优于向上排序算法。

排序

建完大堆后,我们就要排序了,怎么排序呢?

我们知道,堆顶是最值,所以我们建的大堆堆顶就是最大值,我们把堆顶和堆尾元素交换一下,然后数组大小size - -,剩下的元素再调整为一个大堆,可以类比为出堆操作,一直循环调整,直到size=0为止。

我们简单测试一下:

可以看到成功被排为升序数组,说明代码逻辑没问题。

现在我们可以知道,排成升序数组要建大堆,降序数组就建小堆。

堆排序的时间复杂度为O(nlogn),算是很高效率的排序算法了,我们之前学过的冒泡排序(Bubble Sort)的时间复杂度都为O(n2)。

相关推荐
2401_8414956421 小时前
【数据结构】B+树
数据结构·数据库·c++·b+树·概念·结构·操作原理
皓月斯语1 天前
【C++基础】三目运算符
开发语言·数据结构·c++
剑锋所指,所向披靡!1 天前
数据结构之拓扑排序
数据结构
夜不会漫长1 天前
数据结构:链表
数据结构·链表
剑锋所指,所向披靡!1 天前
数据结构之关键路径
数据结构·算法
笨笨饿1 天前
101_详解USB协议
java·jvm·数据结构
J-Tony111 天前
【Redis】数据结构&&持久化
数据结构·数据库·redis
来一碗刘肉面1 天前
队列的链式实现
数据结构·c++·算法·链表
奶人五毛拉人一块1 天前
二分算法以及习题讲解
数据结构·算法·二分