当前位置:   article > 正文

堆排序详解(C语言)_堆排序c语言

堆排序c语言


堆的实现这篇文章的最后,讲解了堆排序的一种实现。实现算法的时间复杂度为O(N * logN),空间复杂度为O(N),总的来说该算法不是特别优秀,还可以将算法优化到空间复杂度为O(1)。

在优化前需要知道空间复杂度为O(1)的两种对数组建堆的算法。

对数组建堆的两种算法

向上建堆

从数组的第二个元素开始,向上建堆。之前实现堆时写过一个向上调整的接口,AdjustUp(HDataType* data, size_t child),该接口将data数组中以child为下标的元素向上调整,调整后的数组从0到child下标之间的数在逻辑上可以看作一个堆。假设我们要建立的堆是小堆,该接口会比较子节点与父节点的大小,子节点小于父节点,交换两节点。

如下图的数组
在这里插入图片描述
第二个节点向上调整后
在这里插入图片描述

第三个节点6大于其父节点,不交换;第四个节点为3,其父节点9,子节点小于父节点,交换。
在这里插入图片描述
在这里插入图片描述

最后被换到根的位置…循环下去直到遍历所有的数,这样对数组的建堆并且其空间复杂度为O(1)的算法就完成了。
在这里插入图片描述
验证堆是否正确
在这里插入图片描述

向下建堆

利用之前写的AdjustDown接口,AdjustDown(HDataType* data, size_t size,size_t root),该接口将root向下调整,使得数组从以root为下标的元素到最后一个元素之间构成一个堆。由于叶节点没有子节点,无法向下调整,所以只需要从数组中第一个叶子节点的前一个节点开始,调用AdjustDown接口,往回遍历数组,这样空间复杂度为O(1)的建堆算法就完成了。

在这里插入图片描述

第一个叶子节点的前一个节点,也就是最后一个节点的父节点。size是数组的大小,size-1是最后一个节点的下标,根据之前的知识,((size - 1) - 1 )/ 2得到最后一个节点的父节点。
在这里插入图片描述

先对2向下建堆,这样第一个圈就是一个小堆,再对3向下建堆,这样第二个圈就是一个小堆,接着对6向下建堆,这样第三个圈就是一个小堆
在这里插入图片描述

接着对最后两个数向下建堆

在这里插入图片描述
循环进行五次,从2开始,到9结束。
在这里插入图片描述
验证结果是否为小堆
在这里插入图片描述

两种建堆算法的时间复杂度

用向上调整算法建堆时间复杂度

在这里插入图片描述
假设一个堆有h层,每一层的节点数都是有规律的。在向上调整建堆的算法中,数组中的每一个节点都要与其父节点进行比较,从最坏的情况考虑,节点会一直比较到根节点。

则向上调整的累计次数:T(n) = 2^1 * 1 + 2^2 * 2 + 2^3 * 3 + … + 2^ (h - 1) * (h - 1),可以理解成每层的节点个数乘以要比较的次数

利用错位相减法,将T(n) * 2 - T(n)得到2^h * (h - 2) + 2,h是堆的层数,2^h - 1 = n,所以h = log(n + 1)。带入结果,(n + 1) * (log(n + 1) - 2) + 2。用渐近表示法,得到向上调整算法的时间复杂度为O(n * logn)。

用向下调整算法建堆时间复杂度

在这里插入图片描述
由于叶节点不用调整,所以要调整的节点范围在倒数第二层往上到根节点。第一层节点要调整的次数:2^0 * (h - 1),第二层:2^1 *(h - 2)…到第h - 1层:2^(h - 2) *1。

T(n) = 2^0 * (h - 1) + 2^1 *(h - 2) + … + 2^(h - 2) *1

用错位相减法得到T(n) = 2^h - h - 3。将h化为n得到:n - log(n + 1) + 2。所以向下调整算法的时间复杂度为O(n)。

堆排序

升序排序

首先排序的前提是:不开辟额外的空间,在原数组中排序。

一个数组要以升序的方式排序,是建立大堆还是小堆?如果建立小堆,堆顶(也就是数组的首元素)是最小的数,取出第一个元素之后,要保证第二个元素是最小的,只能将剩下的元素再建成一个小堆,若用向下调整算法(时间复杂度为O(n)),对一个有n个元素的数组以建立小堆的方式排序,每次取堆顶的数,将其他数再建立成一个小堆,再取堆顶的数,这样循环,时间复杂度为O(n^2),一个算法的时间复杂度是指数次,那这个算法就是垃圾。还不如遍历数组找最小,将最小的数放到数组的前面,两者的时间复杂度一样,但是显然后者更简单。所以建立小堆来升序排序是行不通的。

所以升序排序建立大堆,大堆的根为数组中最大的数,将其与数组最后一个数交换,此时的堆顶是交换后的数,需要再次调整,保证除了最后的数,剩下的数是一个大堆。这样重复下去,这种算法的好处就是没有改变其他数之间的父子关系,而重新建立小堆是改变了所有数之间的父子关系,时间开销大。

在这里插入图片描述
如图是一个大堆,在升序排序前将数组建立成一个大堆。将第一个数9,与最后一个数3交换,数组的长度要减1,此时9不再是堆中的一员
在这里插入图片描述

红线部分是要建立的大堆,此时除了堆顶3,3的左子树与右子树仍然是一个大堆
在这里插入图片描述

绿色部分依旧满足大堆的性质。所以对堆顶的3进行向下调整后,除了9之外的数又组成了一个大堆,堆顶的数又是堆中最大的了,再把堆顶的数与最后一个数交换,再调整堆顶…这样循环下去直到数组的长度为1,此时剩下一个数也没节点和他交换,所以循环结束。

代码实现

void Swap(HDataType* num1, HDataType* num2)
{
	HDataType tmp = *num1;
	*num1 = *num2;
	*num2 = tmp;
}

//建大堆的向下调整
void AdjustDown(HDataType* data, size_t size, size_t pos)//pos是要进行向下调整的数的下标
{
	assert(data);
	
	size_t parent = pos;
	size_t child = parent * 2 + 1;
	while (child < size)//若孩子节点的下标小于数组长度,则循环继续
	{
		if (child + 1 < size && data[child + 1] > data[child])//若右孩子	存在并且右孩子大于左孩子,则修改child,将child指向右孩子
		{
			child++;
		}
		if (data[parent] < data[child])//若父节点小于子节点,交换两节点
		{
			Swap(&data[parent], &data[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;//若父节点大于子节点,满足堆的性质,不用调整
		}
	}
}

void HeapSort(HDataType* data, size_t size)
{
	assert(data);
	for (int i = (size - 2) / 2; i >= 0; i--)
	{
		AdjustDown(data, size, i);
	}
	while (size > 0)
	{
		Swap(&data[0], &data[size - 1]);
		size--;
		AdjustDown(data, size, 0);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

降序排序

排升序用大堆,降序的话则是用小堆。先用数组的数建立一个小堆,将堆顶与数组的最后一个数交换,交换后将除了最后一个数之外的其他数看成一个小堆,当然此时的堆顶不满足小堆的性质,所以需要对堆顶进行向下调整。过程与升序排序类似。

代码实现

void Swap(HDataType* num1, HDataType* num2)// 交换两数的接口
{
	HDataType tmp = *num1;
	*num1 = *num2;
	*num2 = tmp;
}

// 将长度为size的数组中下标为pos的数向下调整
void AdjustDown(HDataType* data, size_t size, size_t pos)
{
	assert(data);

	size_t parent = pos;
	size_t child = parent * 2 + 1;// 假设左孩子是两节点中较小的
	while (child < size)
	{
		// 若右孩子存在并且右孩子小于左孩子,将child指向右孩子
		if (child + 1 < size && data[child + 1] < data[child])
		{
			child++;
		}
		// 若父节点大于子节点,交换两数
		if (data[parent] > data[child])
		{
			Swap(&data[parent], &data[child]);
			parent= child;
			child = parent * 2 + 1;
		}
		else// 父节点小于子节点,满足小堆性质,不交换
		{
			break;
		}
	}
}

void HeapSort(HDataType* data, size_t size)
{
	assert(data);
	
	for (int i = (size - 2) / 2; i >= 0; i--)// 将数组中的数建成一个小堆
	{
		AdjustDown(data, size, i);// 从叶节点的前一个节点开始向下调整
	}

	while (size > 1)	
	{
		Swap(&data[0], &data[size - 1]);
		size--;// 最后一个数不算是堆中的数
		AdjustDown(data, size, 0);// 调整堆顶元素
	}
}
	
```
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/615333
推荐阅读
相关标签
  

闽ICP备14008679号