数据结构 【堆排序】

上面两篇文章中,我们可以了解到大堆的堆顶存放的是堆中最大的元素,小堆的堆顶存放的是堆中最小的元素。利用堆的这个属性就可以实现排序的目的。

如果排升序,那么使用大堆选取堆中最大的数据放置在堆顶,将堆顶元素与堆中最后一个元素进行交换,将最大的元素排除在外进行向下调整,循环遍历整个堆就可以得到原数据的升序排列。如果排降序,同样的道理,先选取小堆的堆顶,得到堆中最小的元素,将其与堆中最后一个元素进行交换位置,将最后一个元素排除在外,再进行向下调整,遍历整个堆,就会得到原数据的升序排列。总结一下:由于堆的特殊属性,排升序时使用大堆,排降序时使用小堆。

在排序之前我要声明的是,不是要建立一个堆这样的数据结构来进行排序。而是进行模拟堆的属性进行排序。期望是有一组存放在数组中的数据,通过堆排序函数实现排序的目的。由于升序降序的核心思想是一致的,这篇文章选取模拟建大堆的过程实现数据的升序排序。下面正文开始!

1、准备工作

先给出本次建堆的需求:将一组存放在数组当中的数据进行升序排序,期望输出原数据的升序排列。

cpp 复制代码
#include <stdio.h>

int main()
{
	int arr[] = {5,3,7,8,2,4,1,6,0,1,-1,-100,50 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	HeapSort(arr, sz);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

2、堆排序

在堆实现这篇文章中我提到堆在插入后要进行向上调整已保证堆的属性:大堆堆顶为最大的数据,小堆堆顶为最小的数据。现在我们假设数组的第一个元素就是堆顶,需要做的是将从第二个元素到最后一个元素进行向上调整以获得堆顶元素,升序要建大堆,我们这里就是为了获得最大的元素。

2.1、模拟向上调整建堆

这里将向上调整封装成一个函数,方便后面的调用。

cpp 复制代码
void Swap(int* ps1, int* ps2)
{
	int tmp = *ps1;
	*ps1 = *ps2;
	*ps2 = tmp;
}

void AdjustUp(int* arr,int child)
{
	assert(arr);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

这里的逻辑和上文中是一致的,当子结点的数据小于父结点的数据时就进行交换,当子结点的下标小于等于0时就跳出循环。这里我们就可以将数据建成一个大堆并可以获取原数据中最大的元素。

2.2、 模拟向下调整排序

这里将向下调整封装成一个函数:

cpp 复制代码
void AdjustDown(int* arr, int size, int parent)
{
	assert(arr);
	int child = parent * 2 + 1;//默认左边的子结点最大
	while (child < size)
	{
		if (child + 1 < size && arr[child + 1] > arr[child])
		{
			child++;//右边子结点存在且满足条件进行调整
		}
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
	
}

如果排升序,那么使用大堆选取堆中最大的数据放置在堆顶,将堆顶元素与堆中最后一个元素进行交换,将最大的元素排除在外进行向下调整,循环遍历整个堆就可以得到原数据的升序排列。

cpp 复制代码
void HeapSort(int* arr, int sz)
{
	assert(arr);
	int i = 0;
	for (i = 1; i < sz; i++)
	{
		AdjustUp(arr, i);
	}

	int end = sz - 1;
	for (i = end; i > 0; i--)
	{
		Swap(&arr[0], &arr[i]);//将堆顶与末尾进行交换
		AdjustDown(arr, i,0);//这里注意堆的尾结点位置为i
	}

}

那么现在所有的工作已经准备就绪,运行程序可以得到结果:

3、时间复杂度

3.1、向上调整

向上调整是从堆的最后一个元素进行的,假设最后一层元素的高度为h,那么这层结点最坏的情况下需要向上交换h-1次(因为它的上面还有h-1层)。假设整个向上调整的次数为T(N),那么T(N)=每一层结点各自的调整次数之和。

这里使用错位相减法求T(N):

所以带入h的值,有:

3.2、向下调整

与向上调整不同的是,向下调整从倒数第二行开始,这就注定着它的时间复杂度要优于向上调整。那么整个向下调整的次数T(N)为:

4、堆排序优化

根据上面复杂度的计算,我们发现向下调整要优于向上调整,实际上在建堆的过程中可以完全使用向下调整。思路如下:

找到最后一个结点的父节点,进行子树的向下调整得到子树的最大值。依次按照图中的顺序遍历每个子树就可以得到最大值。

代码实现如下:

cpp 复制代码
void HeapSort(int* arr, int sz)
{
	assert(arr);
	int i = 0;
	int end = sz - 1;
	for (i = (end - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, sz, i);//父结点的下标为i
	}

	end = sz - 1;
	for (i = end; i > 0; i--)
	{
		Swap(&arr[0], &arr[i]);//将堆顶与末尾进行交换
		AdjustDown(arr, i,0);//这里注意堆的尾结点位置为i
	}

}

上述代码在建堆过程中可以适当优化代码。但整体代码的时间复杂度仍为O(N*logN)

相关推荐
闻缺陷则喜何志丹4 分钟前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
Lenyiin22 分钟前
01.02、判定是否互为字符重排
算法·leetcode
鸽鸽程序猿38 分钟前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd38 分钟前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo61742 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v1 小时前
leetCode43.字符串相乘
java·数据结构·算法
A懿轩A2 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
古希腊掌管学习的神2 小时前
[搜广推]王树森推荐系统——矩阵补充&最近邻查找
python·算法·机器学习·矩阵
云边有个稻草人2 小时前
【优选算法】—复写零(双指针算法)
笔记·算法·双指针算法
半盏茶香2 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法