【数据结构取经之路】建堆&堆排序

目录

引言

建堆的两种方法

一、向上调整建堆

二、向下调整建堆

两种建堆方式的性能比较

堆排序

堆排序的思想

堆排序的时间复杂度

堆排序的空间复杂度

堆排序代码


 

引言

首先,介绍一下本次的主人公——堆。堆是一种数据结构,在逻辑上是一棵二叉树,在物理上是一维数组。堆,可以作为堆排序的前提,还可以有效解决TopK问题。学会建堆是学会堆排序的必要前提,因次,在讲堆排序之前,我们先聊聊如何建堆。

建堆的两种方法

不管是向上调整算法还是向下调整算法,都是有一个重要前提的。如果要调成小堆,那么在插入数据之前,要保证原有的二叉树是小堆:如果要调成大堆,要保证在插入数据之前,原有的二叉树是大堆。

请看上图,预期是插入77后调为大堆。首先的明白一点,77只会沿着它的祖宗往上调直到根,也就是图中的红色线路,右边是调整完以后得到的二叉树,它根本不是大堆,原因就在于插入77之前原有的二叉树不是大堆,调完后,只有红色线路的满足大堆的大小关系,但其他部分并不满足。所以才会要求,插入数据前,原有二叉树是大堆。预期是调为小堆也同理。

一、向上调整建堆

向上调整建堆就是在模拟向堆插入数据的过程。下面将以建大堆为例。

第一个数据插入时,堆里只有一个元素,不需要调整,而且,单个结点既可以认为是大堆,也可以认为是小堆,满足向上调整的前提。当插入19时,因为19 > 11,所以需要向上调整,确保是大堆,插入9时,9 < 19,不需要调整 ,插入再多的数据也同理。因为堆的底层是用数组实现的,所以我们可以通过控制数组的下标来控制二叉树,即控制堆。请看代码:

#include <stdio.h>

void Print(int* a, int n)
{
	for (int i = 0; i < n; i++) printf("%d ", a[i]);
	printf("\n");
}

void Swap(int* px, int* py)
{
	int tmp = *px;
	*px = *py;
	*py = tmp;
}

void AdjustUp(int* a, int child)
{
	int parent = (child - 1) / 2;

	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void BuildHeap(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}
}

int main()
{
	int a[] = { 5,1,8,5,};
	int n = sizeof(a) / sizeof(int);
	Print(a, n);
	BuildHeap(a, n);
	Print(a, n);
	return 0;
}

二、向下调整建堆

以调成小堆为例。

如上图,52要向下调整建小堆的前提是,红圈内必须是小堆,即它的左右子树必须是小堆。 同时,还需注意,52是要和它较小的孩子比较的,如果它比它较小的孩子还小,就不用换下来,反之,则需要交换,直到不满足交换条件。

代码:

#include <stdio.h>

void Print(int* a, int n)
{
	for (int i = 0; i < n; i++) printf("%d ", a[i]);
	printf("\n");
}

void Swap(int* px, int* py)
{
	int tmp = *px;
	*px = *py;
	*py = tmp;
}

void AdjustDown(int* a, int n, int parent)
{
	//假设左右孩子中小的那个为左孩子,如果假设错误,下面会修正
	int child = parent * 2 + 1;

	while (child < n)
	{
        //child + 1 < n,判断右孩子是否存在
	    if (child + 1 < n && a[child + 1] < a[child]) child++;

		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void BuildHeap(int* a, int n)
{
	//从最后一个节点的父亲开始向下调整
	for (int i = (n - 2) / 2; i >= 0; i--)AdjustDown(a, n, i);
}

int main()
{
	int a[] = { 4,1,7,4,7,9,0,1 };
	int n = sizeof(a) / sizeof(int);
	Print(a, n);
	BuildHeap(a, n);
	Print(a, n);
	return 0;
}

两种建堆方式的性能比较

上图的满二叉树一共有16个结点,其中,最后一层占了一半,倒数第二层占了四分之一,其他的满二叉树也是这种情况。 

向上调整算法:假设二叉高度为h,除去第一层不用向上调整,在最坏情况下,其余各层的每一个节点都需要向上调整。因为向上调整的次数与层数有关,所以设前h层需要调整次数的函数为F(h).

F(h) = 2 * 1 + 2^2 * 2 + ……+ 2^(h-1) * (h-1)  =  2^h * (h - 2) + 2  

h = log(N + 1)  ==> N = 2^h - 1 

得到近似值 F(N) = N*logN

所以,向上调整建堆的时间复杂度为:O(n*logn)  

向下调整算法:

F(h) = 2^(h - 2) * 1 + 2^(h-3) * 2 + ……+ 2 * (h - 2) + 2^0 * (h - 1)

h = log(N + 1)  ==> N = 2^h - 1 

F(N) = N - log(N + 1)

取近似值:F(N) = N

所以,向下调整建堆的时间复杂度为:O(n)

通过分析,我们可以看到,向上调整算法,节点数量越多,调整次数(层数越深)越多;向下调整算法,节点数量越多,调整次数越少。从这一角度定性分析,也可以得出向下调整算法较优的结论。

堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。

堆排序的思想

以上图为例,将堆顶元素与最后一个元素交换,此时,最大的元素已经被选出并放到了数组的尾部,接着,再调用向下调整算法,对0到n-1下标的元素进行向下调整,调成大堆后,继续将堆顶元素交换到尾部n-1的位置,此时选出了次大的数……重复上述调整交换的操作,直到只有一个元素。

升序建大堆,降序建小堆。下面以大堆为例说明原因。

大堆,每次均把堆顶的元素交换到了尾部。第一次交换时,把最大的交换到了下标为n-1的位置,第二次交换时,把次大的交换到了下标为n-2的位置,这样,待排完序后,数组就成了升序。归根结底,是因为有交换这一操作。降序用小堆同理。 

堆排序的时间复杂度

O(n*logn)

堆排序的空间复杂度

堆排序是一种原地排序算法,不需要额外的空间来存储数据,所以它的空间复杂度为O(1)

堆排序代码

#include <stdio.h>

void Print(int* a, int n)
{
	for (int i = 0; i < n; i++) printf("%d ", a[i]);
	printf("\n");
}

void Swap(int* px, int* py)
{
	int tmp = *px;
	*px = *py;
	*py = tmp;
}

void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;

	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child]) child++;

		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[end], &a[0]);
		AdjustDown(a, end, 0);
		end--;
	}
}

int main()
{
	int a[] = { 4,1,8,4,6,0,1,7,3,9 };
	int n = sizeof(a) / sizeof(int);
	Print(a, n);
	HeapSort(a, n);
	Print(a,n);
	return 0;
}

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值