m数据结构 day24 排序(三)选择类(简单选择,堆排序)

简单选择排序

simple selection sort

基本思想:以炒股为喻,冒泡像炒短线,选择则是炒长线

在这里插入图片描述

选择排序的思想是:

每一趟在 n − i + 1 ( i = 1 , 2 , ⋯   , n − 1 ) n-i+1(i=1,2,\cdots,n-1) ni+1(i=1,2,,n1)个记录中选关键字最小的一个记录作为有序序列的第一个记录

代码:交换次数非常少

void SelectSort(SqList * L)
{
	int i, j, min;//min是最小值下标
	for (i=1;i<L->length;++i)
	{
		min = i;//最小值下标先初始化为当前下标
		for (j=i+1;i<L->length;++j)
		{
			if (L->r[min] > L->r[j])
				min = j;
		}
		if (i!=min)//则需要交换
			swap(L, i, min);
	}
}

时间复杂度: O ( n 2 ) O(n^2) O(n2)

从代码就可以看到,选择排序的交换次数很少,只有确定是最小值才交换,避免了很多无效的交换,因此就比冒泡排序好的多。

  • 比较次数
    第i趟排序需要n-i次比较。所以总共需要的比较次数:
    ∑ i = 1 n − 1 = n − 1 + n − 2 + ⋯ + 1 = n ( n − 1 ) 2 \sum_{i=1}^{n-1}=n-1+n-2+\cdots+1=\frac{n(n-1)}{2} i=1n1=n1+n2++1=2n(n1)

而,没有最好最坏情况,不管什么情况下,比较次数都一样多。

  • 交换次数
    最好情况(排序表本身有序):不用交换
    最坏情况(逆序):交换n-1次

所以总的时间复杂度是 O ( n 2 ) O(n^2) O(n2)

虽然选择排序和冒泡排序的时间复杂度一样,但是选择排序还是比冒泡排序的性能好一些,毕竟交换次数少了很多啊。

堆排序:充分利用了完全二叉树的深度和序号信息

heap sort

1964年,Floyd 和 William一起发明的,还发明了“堆”这种数据结构。

这里说堆,是说一种数据结构,具体定义见后文,不是内存里的那个“堆”(一块自由存储内存池,可以被程序员自由分配和使用)。

我觉得堆排序的思想整体理解起来还是很简单的,但是要设计这么一种排序,还专门设计了“堆”这种数据结构,这就太难了!

堆排序的思想(以降序排序为例):把记录表构造为一个大顶堆,然后就可以以O(1)取出一个最大值,即根节点的值,然后把剩余节点重新构造为一个大根堆,再取出根节点的值,,反复执行,直到树中只剩下一个元素。

可以看到,每次从根节点取元素,其实就是一个“选择”的行为和过程,和选择排序的基本思想是一样的,只不过执行选择这个动作的方法完全不一样,直接选择是通过比较,而堆排序是通过构造大顶堆或者小顶堆结构实现的选择。这种差别对实际排序效率的改进非常巨大!

堆 heap

先介绍堆这种数据结构。

堆,是一棵完全二叉树,且每个节点的值都大于其两个孩子节点的值(大顶堆/大根堆),或每个节点的值都小于两个孩子的值(小顶堆/小根堆)。

如下图,左为大顶堆,右为小顶堆。
在这里插入图片描述

由于堆是完全二叉树,所以完全二叉树的性质就可以用上了。最基本的当然就是编号,完全二叉树的编号按照层序遍历来的,树中节点的编号从1开始,一定有在这里插入图片描述
再结合上堆结构的大小关系,则

在这里插入图片描述

如果把树中节点按照层序遍历的顺序存入到数组中,下标从1开始,0空着,则上面的公式对于数组就会是

在这里插入图片描述

堆排序的原理

在这里插入图片描述

之所以和末尾元素交换,是为了方便数组操作,把最大值移到末尾,再把数组的前n-1个元素构造为大顶堆,再把新大顶堆的根移到末尾····

原理知道了,也可以很容易分析出,实现的细节上有两点非常难和关键:

  • 如果把一个无序的记录表构建为一个堆?
  • 如果把去除根节点后的剩余元素构造为一个新的堆?

堆排序的例子

明白了,看了得半个小时,直接看代码还是不行,得自己拿一个例子挨句代码执行并结合注释才能看懂

给定一个无序记录表数组后,我们就用层序遍历的顺序把这个数组当做一棵二叉树,即根本不需要真的用二叉链表实现树结构,完全二叉树结合层序遍历就是这么优秀,直接用数组存就行,因为序号必须是按照层序遍历的序号来的。

先看看堆怎么创建:很巧妙,直接从无序表的一半处的节点,按层序遍历顺序倒着遍历节点,对每一个节点为根的子树调整保证根大于两个孩子就行。

比如数组5 1 9 3 7 4 8 6 2,按照层序遍历是下图最左侧的样子,从总长度的一半,即第4个节点开始,对每个节点为根的子树构建大根堆,即让根大于孩子。为什么从4开始呢?因为4-3-2-1这四个节点都有孩子!!!,这就是利用了完全二叉树的性质了,没孩子的节点根本不用管啊,爽歪歪。
在这里插入图片描述
40-10-90-30都有孩子!所以从40开始,倒着挨个调整他们
在这里插入图片描述

堆排序的代码(难,智慧)

void HeapSort(SqList *L)
{
	int i;
	for (i=L->length/2;i>0;--i)
		HeapAdjust(L, i, L->length);
	for (i=L->length;i>1;--i)
	{
		swap(L, 1, i);//把堆顶记录(根)和当前未经排序子序列的最后一个记录交换
		HeapAdjust(L, 1, i-1);//把整个堆调整为大顶堆
	}
}

/*最关键的部分,堆调整,太牛逼了!!*/
/*顺序表L表示的堆中,除了L->r[s]关键字(根)以外,其余所有节点均满足堆的定义*/
void HeapAdjust(SqList * L, int s, int m)
{
	int temp = L->r[s];
	int j;
	for (j=2*s;j<=m;j*=2)//2*s是s的左孩子
	{
		if (j<m && L->r[j]<L->r[j+1])//左孩子小于右孩子
			++j;//j指向关键字较大的孩子
		if (temp >= L->r[j])//如果根大于大孩子,则他已经满足堆定义了,跳过
			break;
		L->r[s] = L->r[j];//根小于大孩子
		s = j;//继续往下找,直到找到关键字最大的后代
	}
	L->r[s] = temp;//把关键字最大后代的位置用节点s的原值替代
}

堆调整函数太智慧了!!!顺序表L表示的堆中,除了 L − > r [ s ] L->r[s] L>r[s]关键字(根)以外,其余所有节点均满足堆的定义。所以新建堆时,要倒着处理分支节点,因为最后一个节点下面一定只有俩孩子,也满足这一点,而倒数第二个节点,由于后面的都处理了,所以也就满足了。因此,构建新堆和调整堆都用这一个函数,太棒了。

时间复杂度分析:最好、最坏、平均情况都是 O ( n log ⁡ n ) O(n\log n) O(nlogn)

  • 构建二叉树:
    虽然是只对一半的节点(分支节点)操作,但是每个有孩子的分支节点都需要两次比较(左孩子和右孩子比,大孩子和根比),所以是O(n)

  • 调整堆的过程:
    完全二叉树的深度是 ⌊ log ⁡ 2 n ⌋ + 1 \lfloor \log_2 n \rfloor+1 log2n+1,堆调整时,每个节点找自己的最大关键字的后代时,最多往下走 ⌊ log ⁡ 2 n ⌋ \lfloor \log_2 n \rfloor log2n步;而需要取堆顶n-1次,即需要调整n-1次。所以乘起来,时间复杂度是 O ( n log ⁡ n ) O(n\log n) O(nlogn)

综合起来,是 O ( n log ⁡ n ) O(n\log n) O(nlogn)

空间复杂度上,也只是需要一个数组位置作为辅助单元,用作交换时的暂存单元。

评价:对原始记录排序情况不敏感;不稳定;不适合记录数少的情况

从代码可以看到,堆调整时,每个节点为了找自己的最大后代,会沿着自己的左孩子一路向左找,所以比较和交换不是像冒泡和简单选择那样挨着挨着的,而是跳跃的,所以也不稳定。

不知道为啥跳跃着比较就不稳定了??

如果记录数很少,那初始化堆,即最初专门建一个堆的时间反而占比更大,很不划算,所以不适合。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值