排序实现与分析-- 堆排序

堆排序

描述

说实话,描述这一块一点头疼,感觉我会说的很乱,但我尽力0.0;

堆排序,又是一个性能很棒的排序这个排序,我个人感觉好高级,速度很快,但是原理要烦躁一些,下边我们慢慢来说。
如下是我们这次的待排序数据:
在这里插入图片描述
我们把它换成树的样子,就变成了这样:
在这里插入图片描述
这里我们黑色是数据,红色代表他在原数据中的索引,也就是下标,这是未调整的初始状态,接下来我们打断一下,说另一点,堆的问题:
堆有大根堆和小根堆,大根堆就是指整个树中,所有的父节点的值都比孩子节点值大即为大根堆,反之则为小根堆,示例我们下图中“ 7,2,4”值构成的就是7为父节点2、4孩子节点的大根堆,而“1,5,7”这三个点构成小根堆,其余概念我们不多说,继续堆排序
在这里插入图片描述
在这里插入图片描述

这组数据我们的目的是排列为升序数据,那么我们就把这个树调整为大根堆,这里可能有人会有疑问,为什么是调整大根堆,我们稍后解释,我们从后往前从上往下做调整。
我们找到最后一个拥有孩子节点的父节点进行调整,这里我们最后一个符合条件的根节点为索引为3的节点的子树,取出根节点,找出其左右孩子中找到较大值此较大值如果大于父节点值父节点值与此值交换,反之不换,这个时候这颗子树就是一个大根堆了,结果如下:
在这里插入图片描述
然后来到下一颗子树,根节点为索引为2的子树,根节点7取出,然后寻找左右孩子较大值(4),7>4,所以不换,这颗子树也成为大根堆,结果如下:在这里插入图片描述
下一颗子树,根节点为索引为1的子树,取出5,左右孩子较大值9,9>5,交换,结果如下:在这里插入图片描述
最后是根节点为索引为0的树,整棵树,取出根节点,寻找左右孩子最大值(9),9>1,交换,此时1的新左右孩子最大值(8),8>1,继续交换,此时1的新左右孩子最大值6,6>1,继续交换,最终结果为:在这里插入图片描述
此时这颗树调整为大根堆结束,如图所示,所有的父节点值都大于子节点。
接下来就是如何利用它排序的问题了,我们把9和待排序的数据中最后一个数据(3)进行交换,此时9称为有序数列,不参与下次排序,新的树,所有子树都是大根堆,仅有整颗树非大根堆,所以只需对整棵树进行一次堆调整即可,说到这里,我来说为什么升序排序要调整大根堆,成为大根堆,最大值都在树的根节点,然后把根节点后置,其余值继续调整,依次得出的排序结果即为升序数列。

我们第一次最大值后之后结果如下:在这里插入图片描述
然后对整体进行一次堆调整后,结果如下:在这里插入图片描述
最大值后置结果如下:
在这里插入图片描述
一次堆调整结果如下:在这里插入图片描述
最大值后置,堆调整结果如下:
在这里插入图片描述
继续最大值后置,堆调整,结果如下:
在这里插入图片描述
最大值后置,堆调整:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最终结果如上。


代码实现

现在原理我们大概摸清了,实现还是有些问题的:

  1. 调整大根堆:我们调整大根堆每次需要有开始和结尾,因为每次调整前我们需要取出根节点,所以这个操作我们一开始就要做,然后就是计算每个父节点的孩子节点的问题。如下:
    在这里插入图片描述
    仔细观察发现,假设父节点索引为i,那么左孩子索引就是2i+1,右孩子就是2i+2,同时得出左右孩子较大值,当与父节点进行交换时我们需要通过孩子节点推算父节点索引,还是上图不难看出,若是孩子节点索引为i,那么父节点索引就是(i-1)/2*(这里都是取整)*,然后就是我们的循环,每个父节点检测结束增量是什么
    在这里插入图片描述
    如图,需要的整棵树检测,3和较大孩子8交换后,3的新孩子为6、5,再和较大孩子交换后,新孩子为1、9,然后再和9交换,每次我们i都增加至需要交换的孩子节点位置。
    到这里,每次堆调整问题基本解决,代码实现如下:
void heapadjust(int *arr,int start,int end)
{
	int tmp = arr[start];//取出根节点
	int i;
	for(i = 2*start+1;i <= end;i = i * 2 + 1)//i初始化为左孩子,增量为下一层
	{
		if((i+1) <= end && arr[i] < arr[i+1])//寻找左右孩子较大值,并且保证没有chaochu结尾,若是超出,说明没有孩子节点
		{
			i++;//若是左孩子小于右孩子,我们把i的位置更新为较大孩子的索引
		}
		if(arr[i] > tmp)//较大值和父节点进行比较
		{
			arr[(i-1)/2] = arr[i];
		}
		else//其他情况不做处理,跳出循环
		{
			break;
		}
	}
	arr[(i-1)/2] = tmp;//最后,我们把根节点的值放在合适的位置
}

这里最后一句代码我解释一下,因为循环内我们最后把较大值提到了前面,并且根节点的值我们找到了合适位置,但是循环结束我们最后对位置i还进行了自增,增到了合适位置的孩子位置,所以这里我们需要回调

  1. 就是堆排序,堆调整的操作完成,我们在画图的过程中发现,堆排序其实是分两步的,第一步是先进行大根堆建立,然后是循环的进行最大值后置,堆调整一次,所以我们代码逻辑也是分两块的。

    这里我们建立的时候有个问题就是,每次传入的起始位置,我们的最开始是从最后一个拥有孩子节点的父节点的子树开始调整的,这个点其实就是我们最后一个点的父节点,所以根据上边推算父节点的公式我们可以得出i初始值为(len - 2)/2,在第二步的堆调整时候,每次我们都有一个数字有序,排序时候需要将这个数据排出待排序数据中,这个有序数据个数的标识就是我们每次增加的i,实现如下:

void heapsort(int *arr,int len)
{
	for(int i = (len - 2)/2;i >= 0;--i)//堆建立
	{
		heapadjust(arr,i,len);
	}
	
	//堆调整
	for(int i = 0;i < len;++i)
	{
		int tmp = arr[len - 1 - i];
		arr[len - 1 - i] = arr[0];
		arr[0] = tmp;//最大值后置

		heapadjust(arr,0,len-2-i);//排除有序数据的待排数据进行堆调整
	}
}

时间复杂度、空间复杂度及稳定性
  1. 时间复杂度:堆调整,我们每次的i增量式2+1,所以一次堆调整的时间复杂度为logn(log以二为底),n个数据进行n次,所以整体的时间复杂度为O(nlogn)
  2. 空间复杂度:我们申请的变量是常数个,所以是O(1)
  3. 稳定性:因为我们这里的交换是父子节点进行交换,父子的索引号是跳跃的,所以是跳远式交换数据,所以不稳定
完整代码
void heapadjust(int *arr,int start,int end)
{
	int tmp = arr[start];//取出根节点
	int i;
	for(i = 2*start+1;i <= end;i = i * 2 + 1)//i初始化为左孩子,增量为下一层
	{
		if((i+1) <= end && arr[i] < arr[i+1])//寻找左右孩子较大值,并且保证没有chaochu结尾,若是超出,说明没有孩子节点
		{
			i++;//若是左孩子小于右孩子,我们把i的位置更新为较大孩子的索引
		}
		if(arr[i] > tmp)//较大值和父节点进行比较
		{
			arr[(i-1)/2] = arr[i];
		}
		else//其他情况不做处理,跳出循环
		{
			break;
		}
	}
	arr[(i-1)/2] = tmp;//最后,我们把根节点的值放在合适的位置
}

void heapsort(int *arr,int len)
{
	for(int i = (len - 2)/2;i >= 0;--i)//堆建立
	{
		heapadjust(arr,i,len);
	}
	
	//堆调整
	for(int i = 0;i < len;++i)
	{
		int tmp = arr[len - 1 - i];
		arr[len - 1 - i] = arr[0];
		arr[0] = tmp;//最大值后置

		heapadjust(arr,0,len-2-i);//排除有序数据的待排数据进行堆调整
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值