排序——堆排

本文详细介绍了堆排序的原理,包括堆的概念、小堆和大堆特性,以及向上调整和向下调整建堆的方法。对比了两种建堆方式的时间复杂度,并指出堆排序的不稳定性和适用场景。

堆排序

堆的概念及结构

        堆一般是一颗完全二叉树,根据堆的特性,堆又分为小堆和大堆。

                小堆特点:父亲<=孩子,即大的数据往下沉

                大堆特点:父亲>=孩子,即小的数据往下沉

        注意:堆有两个地方都有提到,分别在数据结构和操作系统中。在数据结构中的堆一般是一颗二叉树,数据结构的堆是指存放malloc等动态开辟内存的空间。堆不一定有序,但有序一定是堆。

        堆排序是一种基于堆这个数据结构的排序,根据小堆和大堆特点,来对一组无序的数据进行排序。

堆排序过程分析

        对于一组无序的数据,首先我们要对这组无序数据进行建堆,使他称为小堆或者大堆(根据我们的需要)。建堆又分为两种,分别为向上调整建堆向下调整建堆,下面来分析一下两者的过程。

向上调整建堆分析:O(N*logN)

        向上调整建堆。把一组无序数组数组的第一个元素看成已有的堆,从第二个元素开始,模拟堆插入的过程。从第二个元素开始,往后每次遍历都要向上调整,这样可以确保当我们遍历到新的数时,上面的数已经是堆了,这样才符合堆的规则。时间复杂度为:O(N*logN)

向上调整建堆代码实现:
//向上调整
void AdjustUp(int* a, int child)
{
	int parent = (child - 1) / 2;
	while (child>0)
	{
		//如果是小堆,则是>;把小的往上移
		//如果是大堆,则是<;把大的往上移
		if (a[parent] < a[child])
		{
			swap(&a[parent], &a[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void HeapSort(int* a, int n)
{
	//向上调整建堆,O(nlogN)
	for (int i = 0; i < n; i++)
	{
		AdjustUp(a, i);
	}
}

向下调整建堆分析:O(N)

        向下调整建堆。我们先把一个数组看成堆(实际上不是,因为无序,堆是有限制的)找到这个堆的最后一个非叶子节点,对该非叶子几点进行向下调整,调整结束后往前遍历重复次步骤。(最后一个非叶子几点就是最后一个节点(n-1)的父亲节点(((n-1)-1)/2

        核心在于:当一次向下调整结束后,该节点以及改节点下面的元素已经经过了一次调整,所以肯定是堆了,再往前遍历时,就是对一个一个小堆进行调整,那么最后遍历到下标为0时,就完成了向下调整建堆。时间复杂度为:O(N)

向下调整建堆代码实现(升序):
//向下调整
void AdjustDown(HPDataType* a, int size, int parent)
{
	//先假设左孩子小,不行再换成右孩子
	int child = parent * 2 + 1;
	while (child < size)
	{
		//child+1不能超出数组
		if (child + 1 < size && a[child + 1] > a[child])
		{
			child++;
		}

		if (a[parent]<a[child])
		{
			swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}
void HeapSort(int* a, int n)
{
	//向下调整建堆,O(N)
	for (int i = ((n - 1) - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

}

        注意:向下调整会从我们传进来的parent下标开始,对传进来的数组asizesize一般为a数组的所有元素,因为传进来的parent不一定为0)个元素,往下进行调整,如果不符合预期就交换父亲节点和对应的孩子节点。

        总结:经过上述对堆排序的向上调整建堆和向下调整建堆进行了分析,可以看出向下调整建堆时间复杂度更低,速度更快,所以我们使用向下调整建堆来实现堆排序的核心就是向下调整。        

        注意我们需要建大堆还是小堆。假设我们要把一组数字排成升序,如果我们用小堆,那么这组数字实际上只有堆顶能保证是最小的,无法保证后面的数字是升序的,那么我们只能抛开第一个数不看,重新对下面的数进行建堆了,这样的时间复杂度就为o(N2logN),这样就更慢了。下图是模拟用小堆排升序:

        所以,堆排序实际上是一种选择排序,当我们用小堆时,实际上只是选出了一组数字中的最小的数字,后面的数字的大小都是混乱的。那我们就要用大堆了,大堆能选出最大值,然后我们把最大值与最后一位互换位置,最大值就移到了正确的位置,后面排序时就可以把最后一位剔除了,然后对于新的数组,堆顶的左子树和右子树顺序没有发生改变,又是两个大堆,最后就再用一次向下调整就行了。重复上面的操作就能得到一组升序的数字了。

        由于我们没有重新建堆,只是在减少调整的数据的同时加了一个向下调整,所以时间复杂度应该为O(2*NlogN),然后2可以被省略,所以最终的时间复杂度为:O(NlogN)。下图是模拟用大堆排升序:

        总结:堆排序,排升序建大堆,拍降序建小堆

堆排序整体过程分析:

        先使用向下调整建堆,得到一个小堆或者大堆。向下调整建堆要从最后一个叶子节点开始,也就是最后一个节点的父亲节点。建堆后,我们已经找出了原数组的最值在堆顶),把它与最后一位元素交换,再对arr数组,除了被交换下去的值,前面的元素所有元素重新进行向下调整,再得到一个堆,再交换,重复操作,直到遍历到数组首元素,再调整完,就说明整个数组已经排序完毕。

        时间复杂度为:O(N*logN);空间复杂度为:O(1);由于堆是的性质是,只有父亲节点和孩子节点有大小关系,兄弟节点没有大小关系,所以在建堆或排序的时候,就可能导致相同大小的数的相对位置改变,所以堆排序是不稳定排序

堆排序代码:
//向下调整
void AdjustDown(int* a, int size, int parent)
{
	//先假设左孩子小,不行再换成右孩子
	int child = parent * 2 + 1;
	while (child < size)
	{
		//child+1不能超出数组
		if (child + 1 < size && a[child + 1] > a[child])
		{
			child++;
		}

		if (a[parent]<a[child])
		{
			swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}
//堆排序
void HeapSort(int* a, int n)
{
	//升序,建大堆

	//向下调整建堆,O(N)
	for (int i = ((n - 1) - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	//把堆顶和堆底交换,再向下调整
	for (int i = 0; i < n; i++)
	{
		//堆顶和尾部交换,每交换一次,就说明当前堆的最大值被移到了最后面
		swap(&a[0], &a[n - 1 - i]);
		//每交换一次就减小一次堆
		AdjustDown(a, n - 1 - i, 0);
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值