【排序算法】四、堆排序(C/C++)

「前言」文章内容是排序算法之堆排序的讲解。(所有文章已经分类好,放心食用)

「归属专栏」排序算法

「主页链接」个人主页

「笔者」枫叶先生(fy)

目录

  • 堆排序
    • 1.1 原理
    • 1.2 堆的向下调整
    • 1.3 堆排序代码实现
    • 1.3 性质总结

堆排序

1.1 原理

概念介绍

堆是一种特殊的树形数据结构,它满足以下两个性质:

  1. 堆是一棵完全二叉树
  2. 堆中每个节点的值都必须大于等于(或小于等于)其子节点的值,这样的堆称为大根堆(或小根堆)

堆排序是一种基于二叉堆数据结构的排序算法,堆排序一般都是使用数组(顺序表)的结构进行排序(顺序存储)

堆排序算法的核心就是利用堆的性质来实现排序,堆这里就不详细介绍了(在数据结构——堆中已经详细介绍)

堆排序采用的是堆的向下调整算法(为什么选这个,在数据结构——堆中已经详细介绍)

  • 堆排序想要对排序的数进行升序排序:建小根堆(小堆)
  • 想要对排序的数进行降序排序:建大根堆(大堆)
  • 小根堆:在小根堆中,任意节点的值都小于或等于其子节点的值(最小值在根节点)
  • 大根堆:在大根堆中,任意节点的值都大于或等于其子节点的值(最大值在根节点)

堆排序的构建步骤

  1. 先构建堆:将待排序的序列构建成一个大堆(或小堆)
  2. 再调整堆:将堆顶元素与堆的最后一个元素交换,并重新调整堆,使得剩余元素仍然满足堆的性质
  3. 重复步骤2,直到所有元素都排好序

注意需要注意的是排升序要建大堆,排降序建小堆

下面介绍堆的向下调整

1.2 堆的向下调整

堆向下调整算法的基本思想是将堆中的某个节点按照堆的性质向下调整,使得以该节点为根的子树重新成为一个堆。具体步骤如下:

  1. 首先确定需要调整的节点的左右子节点中的较大值(或较小值,根据堆的性质而定)
  2. 将该节点与其左右子节点中的较大值(或较小值)进行比较,如果该节点的值不符合堆的性质(小堆或大堆),则交换两者的位置
  3. 继续对交换后的节点进行向下调整,直到该节点的值符合堆的性质或者已经没有子节点可以进行比较为止

通过这样的向下调整操作,可以保持堆的性质不变,并且在插入或删除节点之后,可以快速地恢复堆的性质

例如(以小堆为例)

  1. 从根结点处开始,选出左右孩子中值较小的孩子
  2. 让小的孩子与其父亲进行比较
  3. 若小的孩子比父亲还小,则该孩子与其父亲的位置进行交换
  4. 并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止
  5. 若小的孩子比父亲大,则不需处理了,调整完成,整个树已经是小堆

注意:向下调整算法有一个前提:左右子树必须是一个堆,才能调整

假设向下调整27,如图所示:
在这里插入图片描述
堆的向下调整算法代码:

降序:小堆

void Swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}// 堆的向下调整(下面是小堆:降序)
void AdjustDown(int* arr, int n, int parent) // n:arr数组的大小; parent:父节点的数组下标
{// 左子节点下标为parent * 2 + 1; 右子节点的下标为parent * 2 + 2int child = parent * 2 + 1; // 假设左孩子较大// while (child < n){// 选出左右孩子中小的那个if ((child + 1 < n) && arr[child] > arr[child + 1]) // child + 1:右孩子的下标;-- 右孩子存在,并且左孩子比右孩子大{++child;}// 孩子跟父亲比较if (arr[child] < arr[parent]){Swap(&arr[child], &arr[parent]); // 交换数据//迭代parent = child;child = parent * 2 + 1;}else{break;}}
}

:若父节点下标为parent,左子节点下标为parent * 2 + 1,右子节点的下标为parent * 2 + 2

如果要改为升序,要建大堆

修改一下,判断条件即可

// ...
if ((child + 1 < n) && arr[child] < arr[child + 1])
// ...
if (arr[child] < arr[parent])
// ...

向下调整的时间复杂度计算

  • 使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h-1次(h为树的高度)
  • h = log(N+1)(N为树的总结点数,log以2为底)
  • 所以堆的向下调整算法的时间复杂度为:O(logN)

1.3 堆排序代码实现

前面说到,使用堆的向下调整算法需要满足其根结点的左右子树均为大堆或是小堆才行,那么如何才能将一个任意树调整为堆呢?

只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可

// 1.建堆
// 向下调整建堆方式时间复杂度:O(N)
// 从第一个非叶子结点开始向下调整,一直到根
for (int i = (n - 1 - 1) / 2; i >= 0; --i) // n-1:数组下标上限,(n-1-1)/2:孩子的父亲节点
{AdjustDown(arr, n, i);
}

以建小堆为例,如图所示:

  • 第一次向下调整:(非叶子节点开始)
    在这里插入图片描述
  • 第二次向下调整:在这里插入图片描述
  • 第三次向下调整:在这里插入图片描述
  • 第四次向下调整:在这里插入图片描述
  • 第五次向下调整:(到根节点结束)在这里插入图片描述

堆排序分两步:

  1. 建堆
  2. 调整(即排序的过程)

堆排序代码如下:

//堆排序
void HeapSort(int* arr, int n) // n:数组大小
{// 1.建堆// 向下调整建堆方式时间复杂度:O(N)// 从第一个非叶子结点开始向下调整,一直到根for (int i = (n - 1 - 1) / 2; i >= 0; --i) // n-1:数组下标上限,(n-1-1)/2:孩子的父亲节点{AdjustDown(arr, n, i);}// 2.调整(排序的过程)// 调整时间复杂度:O(N*logN)int end = n - 1; // 记录堆的最后一个数据的下标while (end > 0){// 堆顶数据与堆的最后一个数据交换Swap(&arr[0], &arr[end]);// 进行调整AdjustDown(arr, end, 0); // 对根进行一次向下调整end--; // 堆的最后一个数据的下标减一}
}

堆排序的时间复杂度计算

建堆的时间复杂度:

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果)
在这里插入图片描述
计算建堆过程中总共交换的次数:

① T ( n ) = 2 0 ∗ ( h − 1 ) + 2 1 ∗ ( h − 2 ) + 2 2 ∗ ( h − 3 ) + . . . + 2 h − 3 ∗ 2 + 2 h − 2 ∗ 1 ① T(n)=2^0*(h-1)+2^1*(h-2)+2^2*(h-3)+...+2^{h-3}*2+2^{h-2}*1 T(n)=20(h1)+21(h2)+22(h3)+...+2h32+2h21

两边同时乘2得:(等差数列乘以一个等比数列,使用裂项相消法进行化简)

② 2 T ( n ) = 2 1 ∗ ( h − 1 ) + 2 2 ∗ ( h − 2 ) + 2 2 ∗ ( h − 3 ) + . . . + 2 h − 2 ∗ 2 + 2 h − 1 ∗ 1 ②2 T(n)=2^1*(h-1)+2^2*(h-2)+2^2*(h-3)+...+2^{h-2}*2+2^{h-1}*1 ②2T(n)=21(h1)+22(h2)+22(h3)+...+2h22+2h11

②-①两式相减得:(错位相减)

T ( n ) = 1 − h + 2 1 + 2 3 + . . . + 2 h − 2 + 2 h − 1 T(n)=1-h+2^1+2^3+...+2^{h-2}+2^{h-1} T(n)=1h+21+23+...+2h2+2h1

T ( n ) = 2 0 + 2 1 + 2 3 + . . . + 2 h − 2 + 2 h − 1 − h T(n)=2^0+2^1+2^3+...+2^{h-2}+2^{h-1}-h T(n)=20+21+23+...+2h2+2h1h

运用等比数列求和得:

T ( n ) = 2 h − h − 1 T(n)=2^h-h-1 T(n)=2hh1

由二叉树的性质,有 N = 2 h − 1 N=2^h-1 N=2h1 h = l o g 2 ( N + 1 ) h=log_2(N+1) h=log2(N+1),于是:

T ( n ) = N − l o g 2 ( N + 1 ) ≈ N T(n)=N-log_2(N+1)≈N T(n)=Nlog2(N+1)N

用大O的渐进表示法,即:

T ( n ) = O ( N ) T(n)=O(N) T(n)=O(N)

因此:建堆的时间复杂度为O(N)

排序时间复杂度:

  • 每次进行堆的向上调整时间复杂度为O(logN)
  • 数组一共有n个元素
  • 进行排序的时间复杂度为:O(nlogn)

堆排序的总时间复杂度为O(n + nlogn) = O(nlogn)

1.3 性质总结

  • 时间复杂度:堆排序的平均时间复杂度为 O(nlogn),最坏情况下也为 O(nlogn)
  • 空间复杂度:O(1)
  • 不稳定性:堆排序是一种不稳定的排序算法,即相同元素的相对位置可能会发生变化
  • 适用范围:堆排序适用于大数据量的排序,对于小数据量的排序效率较低

--------------------- END ----------------------

「 作者 」 枫叶先生
「 更新 」 2024.1.11
「 声明 」 余之才疏学浅,故所撰文疏漏难免,或有谬误或不准确之处,敬请读者批评指正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/340944.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

推荐一下最近在看到比较好的小报童

最近订阅了很多优秀的小报童 说真的&#xff0c;在知识付费领域&#xff0c;小报童是一个弟弟&#xff0c;但是这种模式却非常棒 轻量级交付&#xff0c;靠口碑传播 这对于想进入知识付费领域&#xff0c;但是又不想重交付的人来说&#xff0c;确实是一个很好的平台 下面推…

git 中的概念

git 中的概念 在使用 Git 版本控制的过程中&#xff0c;有些概念我们必须有所了解&#xff0c;这样才能更有效率也更有意义的学下去。 有清楚且正确的概念认知&#xff0c;不但有助于我们学习如何操作 Git 命令&#xff0c;更重要的是&#xff0c;学习 Git 的相关知识也会更加…

【【ZYNQ基础模块串口的意义】】

ZYNQ基础模块串口的意义 ZYNQ的配置介绍 如此纯粹的引脚显示 DDR 用来接DDR FIXED_IO 主要用来调试 现在一步一步配置出PS与PL端想沟通的zynq核 第一步给PL端添加一个时钟 因为 PS 与 PL端的时钟晶振不一样 我们可以通过一个PLL 来帮助我们生成想要实现的PL端时钟 第二步…

程序员有哪些接单的渠道?

这题我会&#xff01;程序员接单的渠道那可太多了&#xff0c;想要接到合适的单子&#xff0c;筛选一个合适的平台很重要。如果你也在寻找一个合适的接单渠道&#xff0c;可以参考以下这些方向。 首先&#xff0c;程序员要对接单有一个基本的概念&#xff1a;接单渠道可以先粗略…

gseaplot3修改一下clusterProfiler默认绘图函数

直接使用clusterProfiler::gseaplot2绘图会出现下边的结果&#xff0c;导致四周显示不全&#xff0c;线的粗细也没办法调整&#xff0c;因为返回的是一个aplot包中的gglist对象&#xff0c;没太多研究。 p1 <- clusterProfiler::gseaplot2(gsea_result, gsea_result$ID, pv…

行为型模式 | 观察者模式

一、观察者模式 1、原理 观察者模式又叫做发布-订阅&#xff08;Publish/Subscribe&#xff09;模式&#xff0c;定义了一种一对多的依赖关系。让多个观察者对象同时监听某一个主题对象&#xff0c;这个主题对象在状态上发生变化时&#xff0c;会通知所有观察者对象&#xff0…

Camunda Rest API

客户端像调用本地方法一样调用引擎中的接口。 https://docs.camunda.org/manual/7.17/reference/rest/ 一&#xff1a;pom.xml <dependency><groupId>org.camunda.community.rest</groupId><artifactId>camunda-platform-7-rest-client-spring-boot-…

【Kafka-3.x-教程】-【三】Kafka-Broker、Kafka-Kraft

【Kafka-3.x-教程】专栏&#xff1a; 【Kafka-3.x-教程】-【一】Kafka 概述、Kafka 快速入门 【Kafka-3.x-教程】-【二】Kafka-生产者-Producer 【Kafka-3.x-教程】-【三】Kafka-Broker、Kafka-Kraft 【Kafka-3.x-教程】-【四】Kafka-消费者-Consumer 【Kafka-3.x-教程】-【五…

【Docker】概述与安装

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Docker的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一. Docker的概述 1.Docker为什么出现 2…

【设计模式-7】门面模式的代码实现和应用场景

门面模式&#xff0c;最简单的结构性设计模式&#xff0c;将多个不同的子系统逻辑封装起来&#xff0c;对外提供统一的调用接口。门面模式又叫做外观模式&#xff0c;可能是我们接触最多的模式&#xff0c;在开发中&#xff0c;可能不经意间就用到了门面模式。 1. 概述 门面模…

设计模式之组合模式【结构型模式】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档> 学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某…

极客时间-读多写少型缓存设计

背景 内容是极客时间-徐长龙老师的高并发系统实战课的个人学习笔记&#xff0c;欢迎大家学习&#xff01;https://time.geekbang.org/column/article/596644 总览内容如下&#xff1a; 缓存性价比 一般来说&#xff0c;只有热点数据放到缓存才更有价值 数据量查询频率命中…