堆排序让你改变对选择排序的看法

作者主页:paper jie的博客_CSDN博客-C语言,算法详解领域博主

本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。

本文录入于《算法详解》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造,将算法基础知识一网打尽,希望可以帮到读者们哦。

其他专栏:《系统解析C语言》《C语言》《C语言-语法篇》

内容分享:本期将对八大排序中的堆排序进行详细的讲解,各位看官姥爷快搬好小板凳坐好叭。

    -------- 不要998,不要98,只要一键三连,三连买不了吃亏,买不了上当

目录

前言

什么是堆排序

堆排序的实现

基本思想

具体的代码实现

基本原理

完全二叉树

堆排序的性能

 堆排序的改进


前言

上期我们对简单选择排序进行了详细的介绍,我们发现简单选择排序虽然简单,但是它的复杂度还是比较高,那么今天我们来介绍它的改进版—堆排序。

什么是堆排序

991年的计算机先驱奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德(Robert W.Floyd)和威廉姆斯(J.Williams)在1964年共同发明了著名的堆排序算法。堆排序(是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

堆排序的实现

基本思想

创建一个堆 

把堆首和最后一个元素互换;

把堆的尺寸缩小 1,并继续进行堆排序,目的是把新的数组顶端数据调整到相应位置;

重复步骤 2,直到堆的尺寸为 1。

具体的代码实现

#include <stdio.h>

//交换两个元素 传的是地址,就用指针接收
void swap(int *a, int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//变成堆
void heapify(int* arr, int n, int i)
{
	//父亲
	int dad = i;
	//左孩子
	int lson = 2 * i + 1;
	//右孩子
	int rson = 2 * i + 2;
	//大于父亲就交换 注意不能越界
	if (lson< n && arr[dad] < arr[lson])
	{
		dad = lson;
	}
	if (rson < n && arr[dad] < arr[rson])
	{
		dad = rson;
	}
	if (dad != i)
	{
		swap(&arr[i], &arr[dad]);
		//交换后可能会破坏堆的性质 用递归再进行堆排列
		heapify(arr, n, dad);
	}
	else
		return;

}
//堆排序
void heap_sort(int* arr, int n)
{
	int i = 0;
	//建堆 从最后一个父亲开始
	for (i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		heapify(arr, n, i);
	}
	//排序
	for (i = n - 1; i > 0; i--)
	{
		//交换最后一个和第一个
		swap(&arr[i], &arr[0]);
		//再将它们变成大顶堆
		heapify(arr, i, 0);
	}
}
int main()
{
	//待排序元素
	int arr[] = { 2,34,5,1,0,5,6,3,2,17,4,32,56 };
	//求元素个数
	int sz = sizeof(arr) / sizeof(arr[0]);
	//堆排序
	heap_sort(arr, sz);
	int i = 0;
	//打印
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

基本原理

这里有几个知识点需要说明:

大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列

小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列

我们代码中用的就是大顶堆。这里的大顶堆用的就是完全二叉树,什么是完全二叉树呢?

完全二叉树

二叉树是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个节点最多只能有两棵子树,且有左右之分 。

完全二叉树就是设二叉树的深度为h,除第 h 层外,其它各层的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。一棵二叉树至多只有最下面的一层上的结点的度数可以小于2,并且最下层上的结点都集中在该层最左边的若干位置上,而在最后一层上,右边的若干结点缺失的二叉树,则此二叉树成为完全二叉树。

堆排序的性能

初始化建堆的时间复杂度为O (n),排序重建堆的时间复杂度为nlog (n),所以总的时间复杂度为O (n+nlogn)=O (nlogn)。 另外堆排序的比较次数和序列的初始状态有关,但只是在序列初始状态为堆的情况下比较次数显著减少,在序列有序或逆序的情况下比较次数不会发生明显变化。复杂度的计算过程:

假设目标堆是一个满堆,即第 k 层节点为 2ᵏ。输入数组规模为 n, 堆的高度为 h, 那么 n 与 h 之间满足 n=2ʰ⁺¹ - 1,可化为 h=log₂(n+1) - 1。 (层数 k 和高度 h 均从 0 开始,即只有根节点的堆高度为0,空堆高度为 -1)。

建堆过程中每个节点需要一次下滤操作,交换的次数等于该节点到叶节点的深度。那么每一层中所有节点的交换次数为节点个数乘以叶节点到该节点的深度(如第一层的交换次数为 2⁰ · h,第二层的交换次数为 2¹ · (h-1),如此类推)。从堆顶到最后一层的交换次数 Sn 进行求和:

Sn = 2⁰ · h + 2¹ · (h - 1) + 2² · (h - 2) + ...... + 2ʰ⁻² · 2 + 2ʰ⁻¹ · 1 + 2ʰ · 0

把首尾两个元素简化,记为①式:

①: Sn = h + 2¹ · (h - 1) + 2² · (h - 2) + ...... + 2ʰ⁻² · 2 + 2ʰ⁻¹

对①等于号左右两边乘以2,记为②式:

②: 2Sn = 2¹ · h + 2² · (h - 1) + 2³ · (h - 2) + ...... + 2ʰ⁻¹ · 2 + 2ʰ

那么用②式减去①式,其中②式的操作数右移一位使指数相同的部分对齐(即错位相减法):

化简可得③式:

③ = Sn = -h + 2¹ + 2² + 2³ + ...... + 2ʰ⁻¹ + 2ʰ

对指数部分使用等比数列求和公式:Sn=a1×(1−qn)1−qSn=\frac{a_{1}\times\left( 1-q^{n} \right)}{1-q}

在这个等比数列中,a1=2, q=2,则③式为:

Sn=−h+(2×(1−2h)(1−2))Sn=-h+\left( \frac{2\times\left( 1-2^{h} \right)}{\left( 1 - 2 \right)} \right)

化简为:

④ = Sn = 2ʰ⁺¹ - (h + 2)

在前置条件中已得到堆的节点数 n 与高度 h 满足条件 n=2ʰ⁺¹ - 1(即 2ʰ⁺¹=n+1) 和 h=log₂(n+1) - 1,分别代入④式中的 2ʰ⁺¹ 和 h,因此:

Sn=(n+1)−(log2(n+1)−1+2)Sn=\left( n + 1 \right) - \left( log_{2}\left( n+1 \right) - 1 + 2 \right)

化简后为:

Sn = n - log₂(n + 1)

因此最终可得为 O(n).

 堆排序的改进

因为大多数重新插入堆的元素会直接加入到堆底,发现我们可以免去检查元素是否到达正确位置来节省时间。也就是直接提升较大的子结点直至到达堆底,然后再使元素上浮 🔝 到正确位置。这样几乎可以将比较的次数减半,接近了对随机数组进行归并排序所需要的比较次数。不过需要额外的空间,在实际应用中只会当比较操作代价高时较高时才会使用。改进方法:

将数据初始化为大顶堆,交换第一个和最后一个元素,这里是不变的

重新构造大顶堆是,首先让第一个元素下降h/2的高度(h 为堆的高度)

下降了h/2层后判断这个元素与它的父节点谁大,如果父节点大继续下沉,下沉的结束条件为h=0 如果父节点小,表明第一个元素下沉时走过头了,然后要往回走,进行上浮操作,上浮操作是肯定能够找到第一个元素的最终位置的

循环n-1次程序运行完成。

代码:

void HeapSort_Floyd(void *arr, int n, int size)
{
    for (int k = n / 2; k != 0; --k)
        Sink(arr, k, n, size);
    while (n != 1)
    {
        void * temp = malloc(size);
        memcpy(temp, arr + (n - 1) * size, size);
        Cover(arr, n--, 1, size);
        int k = 1;
        while (2 * k <= n)
        {
            int j = 2 * k;
            if (2 * k < n && LessArr(arr, j, j + 1, size)) ++j;
            Cover(arr, k, j, size);
            k = j;
        }
        memcpy(arr + (k - 1) * size, temp, size);
        Swim(arr, k, size);
        free(temp);
    }
}

  • 28
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 44
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

paper jie

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

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

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

打赏作者

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

抵扣说明:

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

余额充值