<数据结构>堆排序

125 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1. 堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

  1. 把数据一个个放入堆中,每插入一个都潜在的维持了堆的结构(大堆或小堆)

  2. 然后取堆顶数据(最大或最小)放到数组中,再把堆顶数据删除掉,删除之后向下调整,并将目前最大(或最小)的数据挪到堆顶,重复这个步骤直到堆为空。

下面的动图是用这个思路来排序的,但它是从后往前放到数组里的,这个跟我们下面的参考代码有些不同。

参考代码

void HeapSort(int* a, int size)
{
	HP hp;
	HeapInit(&hp);
	for (int i = 0; i < size; i++)
	{
		HeapPush(&hp, a[i]);
	}
	int j = 0;
	while (!HeapEmpty(&hp))
	{
		a[j] = HeapTop(&hp);
		HeapPop(&hp);
		j++;
	}
	HeapDestroy(&hp);
}
int main()
{
	int a[] = { 5,10,6,1,-1,2,7,0 };
	HeapSort(a, sizeof(a) / sizeof(a[0]));
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
	return 0;
}

1.1堆排序的优点和不足

冒泡排序和堆排序的时间复杂度比较:

  • 冒泡排序:(N-1)+(N-2)+·····+2+1:arrow_right:时间复杂度是O(N^2)

  • 堆排序:建堆是O(N * logN),取堆顶数据是O(N):arrow_right:时间复杂度是O(N * logN)

image-20220411175843747

这俩的区别可大着呢!如果不会时间复杂度和空间复杂度请看:数据结构自学教程——算法的时间复杂度和空间复杂度

这样看来,堆排序还是很棒的,但我们一般不会这样使用堆排序,因为它需要另建一个堆来存放数据,空间复杂度是O(N)

那么有没有什么办法,能够在原本的数组上建堆,使用堆排序呢?

1.2 优化:直接在数组上建堆

利用:one:向上调整函数,插入数据的思路建堆或:two:向下调整从下往上建堆

1.2.1 向上调整建堆

//在原数组中向上调整,建堆
for (int i = 1; i < size; i++)//i没必要从0开始,第一次没必要向上调整
{
 AdjustUp(a, i);
}

1.2.2 向下调整建堆

向下调整函数是有要求的,左子树右子数都为大堆(或小堆)才能用。

那样的话就从下往上调,先让子树满足条件,再逐渐向上。

并且叶子节点不用往下调(它们就是最下面的),就从倒数第一个非叶子节点(也就是最后一个节点的父亲)开始调整。

//向下调整,建堆(从倒数第一个非叶子节点开始,也就是最后一个节点的父亲)
for (int i = (size - 1 - 1) / 2; i >= 0; --i)
{
    AdjustDown(a, size, i);
}

1.2.3 时间复杂度对比

:one:向上调整

第二层的2个节点最多要向上调整1次,第三层的4个节点最多要向上调整2次,最后累积,我们会得到下面这个公式

image-20220411211108651

:two:向下调整

第一层的1个节点最多要向下调整h-1次,第二层的2个节点最多要向下调整h-2次,最后累积,我们会得到下面这个公式

image-20220411202700477

1.3 堆排序整体逻辑

提问:能够建小堆实现升序排序?

不能!

建小堆的话最小的数在第一个位置了,接下来要把第二小的数放到第二个位置。

但是将第一个位置的数从堆中除去,剩下的数关系已经乱了,要通过重新建堆才能找到次小的数,时间复杂度为O(N),实在太麻烦了。

同理可得不能建大堆实现降序排序

应该将排好的部分留在数组后部,不影响前方的排序

因此:

  • 升序:建大堆

  • 降序:建小堆

    下面的动图用了向上调整建堆,建大堆实现升序排序

image-20220411212815272

void HeapSort(int* a, int size)
{
	////在原数组中向上调整,建堆
	//for (int i = 1; i < size; i++)
	//{
	//	AdjustUp(a, i);
	//}
	
	//向下调整,建堆(从倒数第一个非叶子节点开始,也就是最后一个节点的父亲)
	for (int i = (size - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, size, i);
	}
    
	size_t end = size - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);//将排完了的数,不看做堆里的数
		end--;
	}

}

int main()
{
	int a[] = { 5,10,6,1,8,2,7,0 };
	HeapSort(a, sizeof(a) / sizeof(a[0]));
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
	return 0;
}

image-20220411220430485