堆排序实现与分析

❤️ 堆排序

💟 堆排序实现思路

堆排序(HeapSort)是利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。

堆分为大根堆和小根堆,并且堆必须为完全二叉树,使用物理上使用数组实现。

  • 大堆:父节点总是大于子节点的堆
  • 小堆:父节点总是小于子节点的堆

✏️ 实现思路:

1、建堆

  • 升序:建大堆
  • 降序:建小堆

2、利用堆删除思想来进行排序

🖊 疑问解决:

1、堆排序时为什么升序要建大堆?降序建小堆?

  • 升序建小堆时,如图:

  • 建好小堆后,我们需要对其调整至升序的状态

①:升序用小堆建好后,只有第一个数据已经排好,为最小的数据,但是剩下的数据是没有排好的,次小的数据也不一定就在下一位,所以需要重新建堆。

②:倘若将剩下的数据看做堆,再重新向下调整,但此时的二叉树不能被看成小堆了已经。如图:

因为向下调整的前提是:左右子树必须是个堆。

③:所以此时就需要重新建堆,选出次小的元素,然后重复①,②、③。

这里的时间复杂度为O(N^2)

  • 升序建大堆时,如图:

  • 建好大堆后,我们需要对其调整至升序状态

①:这里我们可以将最后一个叶子节点的值与堆顶的值进行交换,交换后如图:

②:然后将交换后的最后一个节点不看做堆里面的数据,然后向下调整,选出次大的数据到堆顶,之后再重复①、②

这里的时间复杂度为O(N*logN)

综合以上两种方法,得出升序建大堆时时间复杂度较小,所以升序建大堆,降序同理得建小堆。

2、建堆时算法的选择,是使用向上调整还是向下调整?

  • 向上调整建堆时间复杂度分析

向上调整建堆是从堆顶开始依次向上建堆,我们以满二叉树来分析其时间复杂度

我们假设数的高度为h,分析每一层数据需要向上调整的次数

第1层,2^0个节点,需要向上调整0层

第2层,2^1个节点,需要向上调整1层

第h-1层,2^(h-2)个节点,需要向上调整h-2层

第h层,2^(h-1)个节点,需要向上调整h-1层

总的调整次数 = 每一层节点个数 ×这一层节点最坏向下调整的次数 N = 2^h-1

利用错位相减法算出其时间复杂度为 O(N*logN)

  • 向下调整建堆时间复杂度分析

向下调整建堆是从最后一个节点的父节点开始依次向下建堆,我们以满二叉树来分析其时间复杂度

我们假设数的高度为h,分析每一层数据需要向下调整的次数

第1层,2^0个节点,需要向下调整h-1层

第2层,2^1个节点,需要向下调整h-2层

第h-1层,2^(h-2)个节点,需要向下调整1层

第h层,2^(h-1)个节点,不需要向下调整

所以总的调整次数 = 每一层节点个数 ×这一层节点最坏向下调整的次数

T(N) = 2^0×(h-1) + 2^1×(h-2) + …… + 2^(h-2)×1 N = 2^h-1

利用错位相减法可以计算出T(N) = N-log(N+1)

所以向下调整的时间复杂度为O(N)

很明显,向下调整的时间复杂度低于向上调整的时间复杂度,因此选择向下调整建堆

💜 堆排序实现

void UpAdjusting(HeapDataType* a, size_t child)
{
	size_t parent = (child - 1) / 2;
	//这里的循环判断条件不能为  parent>=0(在取<时,会多走一次循环条件) 或者 parent>0(最后一次不会进入,即parent为0时)
	while (child > 0)
	{
#ifdef SmallHeap
		if (a[child] < a[parent])
		{
			//交换父子节点
			Swap(&a[child], &a[parent]);
			//迭代
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
#endif  //SmallHeap
}   
void DownAdjusting(HeapDataType* a, size_t size,size_t parent)
{
	size_t minchild = parent * 2 + 1;
	while (minchild < size)
	{
		//找出左右孩子最小的一个
		if (minchild + 1 < size && a[minchild + 1] < a[minchild])
		{
			minchild++;
		}
		//判断大小并交换
#ifdef SmallHeap
		if (a[minchild] < a[parent])
		{
			//交换
			Swap(&a[minchild], &a[parent]);
			//迭代
			parent = minchild;
			minchild = parent * 2 + 1;
		}
		else
		{
			break;
		}
#endif
}
void HeapSort(int* a,int size)
{
	//i = size-1-1/2是找到最下面的第一个父节点,size-1是最后一个节点,找父节点是size-1-1
	for (int i = (size-1-1) / 2; i >= 0; --i)
	{
		DownAdjusting(a, size, i); //传i是因为每一棵树都要重新建堆
	}
	//第一个节点和最后一个节点交,然后再恢复小堆结构
	int i = 1;
	while (i < size)
	{
		Swap(&a[0], &a[size - i]);
		DownAdjusting(a, size - i, 0);  
		++i;
	}
}

💚 堆排序时间复杂度分析

堆排序的时间复杂度主要来自于两部分:

  • 对数据进行建堆
  • 对数据调整至有序状态

一、对数据进行建堆

for (int i = (size-1-1) / 2; i >= 0; --i)
{
    DownAdjusting(a, size, i); //传i是因为每一棵树都要重新建堆
}

由上面的分析可以得出对数据建堆的时间复杂度为O(N)

二、对数据调整至有序状态

int i = 1;
while (i < size)
{
    Swap(&a[0], &a[size - i]);
    DownAdjusting(a, size - i, 0);  
    ++i;
}

while循环的时间复杂度为O(N),向下建堆的时间复杂度为O(logN)

所以步骤二的时间复杂度为O(N×logN)

总的时间复杂度为O(N+N×logN) = O(N×logN)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kunmu.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值