8-7选择排序-堆排序

一.回顾
在这里插入图片描述
对此二叉树进行顺序存储(按层序遍历存),图中结点的序号表示数组中存放的位置

i为当前结点序号(从1开始,同数组下标),n表示总的元素个数,当前n=12

其中1-6是叶子结点,因此非叶子结点可表示为1~⌊n/2⌋,叶子结点可表示为⌊n/2⌋+1~n
在这里插入图片描述
对于某个结点i
①i的左孩子的序号应为2i
②右孩子应为2i+1
③父节点为⌊i/2⌋
④所在层次为⌈ l o g 2 ( i + 1 ) log_2{(i+1)} log2(i+1)⌉或⌊ l o g 2 i log_2i log2i⌋+1
⑤如果2i≤n,表示当前结点i有左孩子
⑥2i+1≤n表示有右孩子
⑦如果当前结点i>⌊n/2⌋,说明当前结点是叶子结点
⑧如果当前结点i≤⌊n/2⌋,说明当前结点是分支节点(非叶子)

二.概念引入:堆
在这里插入图片描述
解释:对于一个分支节点i,他的值≥左右孩子的值,称为大根堆。即 根≥左右

在这里插入图片描述
解释:对于一个分支节点i,他的值≤任意左右孩子的值,称为小根堆。即 跟≤左右

如图是一个大根堆的存储视角(左)和逻辑视角(右)

在这里插入图片描述
三.大根堆的建立

从最后一个分支节点开始检查
09→78→17→53
在这里插入图片描述
从09开始,如果其值大于左右孩子,则保存不变,继续看上一个分支节点。否则,找到左右孩子中最大的,与之交换

此处09和32交换
在这里插入图片描述
检查78,和87换
在这里插入图片描述
检查17,和45换
在这里插入图片描述
检查53,和87换
在这里插入图片描述
完成。刚刚降下来的53不满足大根堆,于是53和78换
注:在代码执行时,需要先判断(如果插入)是否满足大根堆,如果满足再插入53。如果不满足,将大的孩子拿上去,再重复对比其孩子
在这里插入图片描述
至此完成了大根堆的建立

代码实现

void BuildMaxHeap(int a[],int len){
		for (int i = len / 2; i > 0; i--)//从最后一个非叶子结点开始构建大根堆
			HeadAdjust(a, i, len);
}

void HeadAdjust(int a[], int k, int len)//建立大根堆
{
	a[0] = a[k];//0作为哨兵,暂存根节点的值
	for (int i = 2 * k; i <= len; i=i*2)//len表示数组长度(从1起),即元素个数
	{
		if (i<len&&a[i] < a[i + 1]) 
			i++;//i指向左右孩子较大的一个,i<len表示确定有右孩子才能+1
		if (a[0] >= a[i]) 
			break;
		else {
			a[k] = a[i];//大的孩子拿上去,当前位置可视为空缺
			k = i;//k指向当前空缺位置。能否将a[0]放入,需要再判断待插入元素和其孩子的关系,看是否满足大根堆,因此继续for循环。详见下图(图1)
		}
	}
	a[k] = a[0];//for循环结束,满足大根堆,a[0]放回来
}

在这里插入图片描述
图 1 图1 1

四.基于大根堆从小到大的选择排序-堆排序

回顾选择排序:选择最大(小)的元素加入有序子序列

将堆顶的87与堆底元素9交换
在这里插入图片描述
此时已将最大的元素放到了正确的位置
在这里插入图片描述
当前在堆顶的09不满足大根堆,调用HeadAdjust(int a[], int k, int len)建立新的大根堆。注意此时的len变为了7(已经确定位置的元素87无需参与)

09按如上的方法下坠,至此完成了第一趟的处理
在这里插入图片描述
第一趟的结果:确定了最大元素的正确位置,将剩余元素恢复成了大根堆

后续的处理同上,经过n-1趟的处理完成了排序

同样小根堆实现的是递减的排序

代码实现(大根堆的堆排序)

#include<stdint.h>
#include<iostream>
using namespace std;
void HeadAdjust(int a[], int k, int len)//建立大根堆
{
	a[0] = a[k];//0作为哨兵,暂存根节点的值
	for (int i = 2 * k; i <= len; i=i*2)
	{
		if (i<len&&a[i] < a[i + 1]) 
			i++;//i指向左右孩子较大的一个,i<len表示确定有右孩子才能+1
		if (a[0] >= a[i]) //和左右孩子中较大的比
			break;
		else {
			a[k] = a[i];
			k = i;//继续下坠
		}
	}
	a[k] = a[0];
}
int main()
{
	int a[6] = { 0,432,21,46,4322,645 };
	int len = 5;
	for (int i = len / 2; i > 0; i--)//从最后一个非叶子结点开始构建大根堆
		HeadAdjust(a, i, len);
	for (int i = len; i > 1; i--)//a[1]放到最后
	{
		swap(a[i], a[1]);
		HeadAdjust(a, 1, i - 1);//剩余元素再构建
	}
	for (int i = 1; i <= 5; i++)
	{
		cout << a[i] << " ";
	}
}

五.效率分析

1.时间复杂度
(1)建堆过程:
每次最多对比关键字两次:左右孩子比较,根和大的那个比较
如果只有左孩子,则只需要一次关键字的对比
如果树高为h,当前结点在第i层,则一共需要下坠h-i层,对比关键字不超过2(h-i)
而h=⌊ l o g 2 n log_2n log2n⌋+1
第i层最多有 2 i − 1 2^{i-1} 2i1个结点,只有第1~h-1层才可能出现元素下坠
因此第一层有 2 1 − 1 2^{1-1} 211=1个结点,下坠共需要1×2(h-1)次关键字对比;第二层共有 2 2 − 1 2^{2-1} 221=2个结点,下坠共需要2×2(h-2)次关键字对比;第h-1层共有 2 h − 2 2^{h-2} 2h2,下坠共需要 2 h − 2 2^{h-2} 2h2×2次关键字对比,带入h,最终结果≤4n

因此建堆的过程,关键字对比次数不超过4n,建堆时间复杂度为O(n)

(2)排序过程
一共需要n-1趟,每趟都要交换两个元素,并进行堆顶元素的下坠,下坠最多h-1层,而每下坠一层最多对比关键字2次。
h=⌊ l o g 2 n log_2n log2n⌋+1,因此下坠的时间复杂度不会超过O( l o g 2 n log_2n log2n),一共n-1趟排序的时间复杂度O(n l o g 2 n log_2n log2n)

因此堆排序的时间复杂度=建堆+排序=O(n l o g 2 n log_2n log2n)

2.空间复杂度O(1)
3.稳定性

if (i<len&&a[i] < a[i + 1]) i++;//i指向左右孩子较大的一个,相等时保持左孩子不变
		if (a[0] >= a[i]) break;

2和1交换,2放到末尾,因此是不稳定的
在这里插入图片描述
过程如下
在这里插入图片描述
在这里插入图片描述
结果
在这里插入图片描述
六.总结
在这里插入图片描述

七.堆的插入和删除

(一)插入
以小根堆为例,插入元素13
在这里插入图片描述
先放到表尾
在这里插入图片描述
13和其父元素32比较,13<32,互换
在这里插入图片描述
13和17交换。对比13和09,无需交换。
在这里插入图片描述
共发生了3次关键字的对比

(二)删除
以小根堆为例,删除元素13
在这里插入图片描述

用堆底元素46填补空白
在这里插入图片描述

然后让46下坠,在其左右孩子中找到更小的结点,与之交换。因此与17交换。
在这里插入图片描述

在这里插入图片描述
46和32交换

在这里插入图片描述
关键字对比次数:两个孩子比1次,和根比一次,共2次(一次下坠)
注:即使不需要交换也需要两个关键字的对比
一共下坠了2次,因此一共对比了关键字4次

如果只有一个孩子,则只需要一次关键字的对比

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

卡__卡

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

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

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

打赏作者

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

抵扣说明:

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

余额充值