数据结构:堆排序

堆排序

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

现在随便给出一组数组:

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)。

相关推荐
白狐_7985 小时前
408数据结构第5章:二叉树遍历序列题——技巧、判断与真题型总结
数据结构
Forever Nore7 小时前
学完C语言力扣第一题做不来正常吗
数据结构·算法
旖旎夜光10 小时前
LeetCode 11:盛最多水的容器(双指针问题) —— 题解
数据结构·c++·算法·leetcode·双指针
不一样的故事12612 小时前
芯片设计是一个高度复杂、分工精细的系统工程
数据结构·经验分享
梁辰兴12 小时前
软件工程:面向数据结构的设计方法
数据结构·软件工程·梁辰兴·面向数据结构的设计方法·jackson方法·warnier方法·方法比较
白狐_79813 小时前
408数据结构第7章:查找②——顺序、折半、分块查找秒杀 + 真题
数据结构·算法
hold?fish:palm14 小时前
链表的基本原理和实现(C++版本)
数据结构·c++·链表
码完就睡15 小时前
数据结构——树、二叉树基础概念
数据结构·算法
疯狂打码的少年16 小时前
【数据结构】栈:定义、顺序栈与链式栈
数据结构·笔记
wabs66616 小时前
关于图论【最短路径之Bellman_ford 算法|卡码网94.城市间货物运输的思考】
数据结构·算法·图论·卡码网·bellman_ford·求最短路径