堆排序算法---C语言实现(超详细解析!!!!)

目录

一、前言

二、堆排序 

 🍎方法一(自己写一个堆,在进行排序)

💦时间复杂度分析 

🍐方法二(直接在数组上建堆) 

💦向上调整建堆 

 💦向下调整建堆 

💦时间复杂度分析(向上建堆和向下建堆熟优?)

 💦升序(排序)建大堆还是小堆?

 💦完整的堆排序(升序)图形和代码演示

三、共勉


一、前言

在上一篇博客,已经详细的讲解了 堆的创建、插入数据、删除数据、销毁,等操作。其实在我们平时的应用中,堆用到的最多的还是-------------堆排序和Topk问题。所以本篇博客,就来详细的讲解以上问题。

如果对----- 堆和二叉树还不太了解的可以取这里看看 O!!

详解二叉树和堆

二、堆排序 

  • 假如我们有一串乱序数组,如下:

       现在想要对它进行排序,按照我们之前学过的知识,想要单纯的实现排序其实并不难,可以直接暴力排序,也可以冒泡排序,甚至使用库函数qsort进行排序……

       但是,既然近期学习了堆,那么堆的一个重要应用就是进行堆排序,这里先简要提下:堆排序即快排的一种。在后面的学习中,我将为大家继续展开其它更多样的快排。今儿个就向各位浅谈下快排之一:堆排序

 🍎方法一(自己写一个堆,在进行排序

✨思路:在上篇博文中,我们模拟实现了堆,实现后即可对一串乱序数组进行堆排序。假设我们排升序,且堆为小根堆。实现过程非常简单。

1.首先,把数组的每个元素(HeapPush)插入到堆中。
2.其次,我们深知小根堆的堆顶是最小的数字,依次遍历堆顶(HeapTop)的元素,将堆顶元素赋值到数组里,从下标0开始,赋值后删除(HeapPop)堆顶元素,++数组下标。此时堆就会重新调整,最终堆顶依旧是最小的,再重复上述赋值堆顶到数组的操作,直到堆为空(HeapEmpty)
⭐:代码如下:

void HPSort(int* a, int n)
{HP ps;  // 定义一个顺序表结构体// 对堆结构进行初始化HPInit(&ps);// 向堆结构中进行尾插数据for (int i = 0; i < n; i++){HPPush(&ps, a[i]);}int i = 0;// 当堆结构不为空时while (!HPEmpty(&ps)){a[i++] = HPTop(&ps);HPPop(&ps);}printf("进行堆排序:\n");for (int j = 0; j < n; j++){printf("%d ", a[j]);}
}int main()
{// 堆排序测试int a[100];int j = 0;int sum ;printf("请输入你要插入堆的数据(-1结束):>\n");for (int i = 0; i < 100; i++){scanf("%d", &sum);if (sum == -1){break;}a[j++] = sum;}HPSort(a, j);return 0;
}

⭐:实验效果图

💦时间复杂度分析 

段一:

for (int i = 0; i < n; i++){HPPush(&ps, a[i]);}

此段代码的时间复杂度为O(N*logN),因为HeapPush函数的内部执行过程就是把数组的每个元素插入堆中,有N次。接着,每插入一个数据都要重新向上调整(AdjustUp)高度次以确保为堆,每个都要调整高度次,高度为logN,综上此段为O(N*logN)

段二:

while (!HPEmpty(&ps)){a[i++] = HPTop(&ps);HPPop(&ps);}

此段的时间复杂度同样为O(N*logN),原理跟上一段类似,不过多赘述。

✨分析:综上,时间复杂度为O(N*logN),确实比我们先前的冒泡排序O(N^2)要快不少。但是,这个方法排序是及其不好的,因为难道说为了实现堆排序还要自己手写一个完整的堆吗?这么复杂的实现堆的过程还不如不用堆排序了这种伤敌一千,自损八百的感脚实在是难受。更何况此法的空间复杂度也是很大的,达到了惊人的O(N)。原因是实现堆的过程是动态开辟的,所以空间复杂度自然是O(N)。可不可以换一更优的方法,但同样是利用堆的思想实现快排呢?


✨我们目前的要求:

  1. 依旧是堆的思想
  2. 时间复杂度O(N*logN)
  3. 空间复杂度O(1)

🍐方法二(直接在数组上建堆) 

先看一下这个乱序数组

既然上文说到可以直接把它看作二叉树,那不妨把逻辑结构画出来看看:

接下来,我们就要进行建堆了,有两种方法:

  1. 使用向上建堆,插入数据的思想建堆
  2. 使用向下调整建堆

💦向上调整建堆 

✨思路:首先,我们把第一个数字看成堆,也就是4,当第二个数字插入进去的时候,进行向上调整算法,使其确保为小堆,向上调整的算法在上篇博文已详细讲解过,不过多赘述。具体插入数据过程就是遍历数组,确保数组里每一个数进行向上调整算法


✨:向上/下调整算法: 详解向上/下调整算法
 

✨:画图演示:(小根堆)

✨:代码演示

void swap(HPDatatype* x, HPDatatype* y)
{int temp = 0;temp = *x;*x = *y;*y = temp;
}void AdjustUp(HPDatatype* a, int child)
{// 计算父亲的小标int parent = (child - 1) / 2;// 当 child 的小标大于 0 就继续 (也就小于是根节点位置)while (child > 0){// 小堆 <// 大堆 >if (a[child] < a[parent]){swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}// 向上调整建堆
void Heap(int* a, int n)
{//建堆int i = 0;for (i = 1; i < n; i++) //应该从i=1时遍历,因为第一个数据在堆里不需要调整,后续再插入时调整{AdjustUp(a, i);}printf("堆:\n");for (int j = 0; j < n; j++){printf("%d ", a[j]);}
}int main()
{// 建堆测试int a[100];int j = 0;int sum ;printf("请输入你要插入堆的数据(-1结束):>\n");for (int i = 0; i < 100; i++){scanf("%d", &sum);if (sum == -1){break;}a[j++] = sum;}Heap(a, j);return 0;
}

✨:效果演示:

符合小根堆的性质

 💦向下调整建堆 

  • 问题:能直接进行向下建堆吗?

答案:不能

解析:首先回顾下使用向下调整的前提是什么?必须得确保根结点的左右子树均为小堆才可,而这里,数组为乱序的,无法直接使用。

  • 解决办法:从倒数第一个非叶结点开始向下调整,从下往上调

✨:分析:从该解决方案中,我们首先要找到这个倒数第一个非叶结点的数在哪?其实最后一个结点的父亲即为倒数第一个非叶结点当我们找到这个非叶结点时,把它和它的孩子看成一个整体,进行向下调整。调整后,再将次父节点向前挪动,再次向下调整,依次循环下去。

  • 再回顾下父亲和孩子间的关系:
  1. leftchild = parent*2 + 1
  2. rightchild = parent*2 + 2
  3. parent = (child - 1) / 2

✨:画图分析

✨:代码演示:建小堆(数据:4 ,2,7,8,5,0,6)

void swap(HPDatatype* x, HPDatatype* y)
{int temp = 0;temp = *x;*x = *y;*y = temp;
}// 向下调整
// 向下调整的前提:后面的数据是堆
void AdjustDown(HPDatatype* a, int n, int parent)
{// 左孩子int child = parent * 2 + 1;// 孩子可能会超出数组范围while (child < n){// 如果右孩子小于左孩子// 找出小的那个孩子// 保证右孩子存在且不越界// 大堆 >// 小堆 <if (child + 1 < n && a[child+1] < a[child]){//跳转到有孩子那里child++;}// 开始向下调整// 大堆 >// 小堆 <if (a[child] < a[parent]){swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}// 向下调整建堆
void Heap(int* a, int n)
{//建堆int i = 0;for (int i = ((n - 1) - 1) / 2; i >= 0; i--){// 给定一个 a 数组,从 i 这个位置进行建堆, n 表示数组的大小AdjustDown(a, n, i);}printf("堆:\n");for (int j = 0; j < n; j++){printf("%d ", a[j]);}
}int main()
{// 建堆测试int a[100];int j = 0;int sum ;printf("请输入你要插入堆的数据(-1结束):>\n");for (int i = 0; i < 100; i++){scanf("%d", &sum);if (sum == -1){break;}a[j++] = sum;}Heap(a, j);return 0;
}

✨:效果演示:

符合小根堆的性质

💦时间复杂度分析(向上建堆和向下建堆熟优?)

首先,我们画张图看下向上和向下建堆后的样子。

从上图中,我们可以看出,使用不同的方式建堆最后的样子是不同的,那哪种方式好呢?

✨:接下来,我将通过时间复杂度的方式为大家解惑:以一颗满二叉树为例:

向上建堆的时间复杂度:

时间复杂度计算的是其调整的次数,根据上文的知识我们已经知晓其是从数组的第二个元素开始的,也就是可以理解为第二层的第一个节点。计算的思想非常简单:计算每层有多少个节点乘以该层的高度次,然后累计相加即可。如下:

通过计算得知:向上建堆的时间复杂度为O(N*logN)

向下建堆的时间复杂度:

向下调整我们前面已经知道它是从倒数第1个非叶节点开始调整的,每层的调整次数为,该层的节点个数*该层高度减1,一直从第1层开始调直至倒数第2层,并将其依次累加,此计算过程和向上调整差不多,都是等比*等差的求和,过程如下:

 通过计算得知:向下建堆的时间复杂度为O(N)

通过上述计算,我们得到如下:

  1. 向上建堆:O(N*logN)
  2. 向下建堆:O(N)

由此可见,使用向下建堆的方式更优,其时间复杂度较小。当然,使用向上建堆也是可以的,只不过向下建堆更好一点。

 💦升序(排序)建大堆还是小堆?

思考:排升序,建小堆可以吗?-- 可以是可以,但没啥意思。
首先对 n 个数建小堆,选出最小的数,接着对剩下的 n-1 个数建小堆,选出第2小的数,不断重复上述过程……。建 n 个数的堆时间复杂度是O(N)所以上述操作时间复杂度为O(N2),效率太低,尤其是当数据量大的时候,效率更低,同时堆的价值没有被体现出来,还不如用直接排序。

【最佳方法排升序,因为数字越来越大,需要找到最大的数字,得建大堆

  1. 首先对 n 个数建大堆。
  2. 将最大的数(堆顶)和最后一个数交换,把最大的数放到最后
  3. 前面 n-1 个数的堆结构没有被破坏(最后一个数不看做堆里面的),根节点的左右子树依旧是大堆,所以我们进行一次向下调整成大堆即可选出第2大的数,放到倒数第二个位置,然后重复上述步骤……。

时间复杂度】:建堆时间复杂度为O(N),向下调整时间复杂度为O(logN),这里我们最多进行N-2次向下调整。

所以堆排序时间复杂度为 :O = O(N) + O(N-2)*O(logN) = O(N*logN),效率是很高的。


✨解决方案:升序建大堆,降序建小堆

 💦完整的堆排序(升序)图形和代码演示

  • 先看下建好大堆的样子:
  • ✨思路:首先,得明确我们建堆后,此时堆顶就是最大的数据,现在我们把第一个数字和最后一个数字交换,把最后一个数字不看做堆里的,只需要数组个数N--即可。此时的左子树和右子树依旧是大堆,再进行向下调整即可。
  • 画图解析过程:
  • ✨:代码演示:
  • #include <stdio.h>
    #include <stdlib.h>
    #include <stdbool.h>
    #include <assert.h>// 数据类型存储
    typedef int HPDatatype;// 数据交换
    void Swap(HPDatatype* x, HPDatatype* y)
    {HPDatatype temp = 0;temp = *x;*x = *y;*y = temp;
    }// HPDatatype* a :表述动态数组
    // n :表示堆内(数组内)的数据个数
    // parent :父节点的下标(开始在根节点)
    void AdjustDown(HPDatatype* a, int n, int parent)
    {// 左孩子int child = parent * 2 + 1;// 防止孩子跑出数组范围(最后一个节点为叶子节点)while (child < n){// 判断左右孩子那个大,将大的与父节点进行交换// child+1 < n 表示确保右孩子存在//  小堆 if (child + 1 < n && a[child] > a[child+1])if (child + 1 < n && a[child] < a[child+1]){// 如果右孩子比左孩子大child++;}// 开始向下调整// 小堆:if (a[child] < a[parent])if (a[child] > a[parent])   // 大堆{Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
    }void HPSort(int* a, int n)
    {// 在原有的数据上(数组)创建一个堆// 从倒数第一个非叶子节点的子树开始调整(最后一个节点的父亲)for (int i = ((n - 1) - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);}// 升序建大堆(根位置和最后的位置交换)int end = n - 1; //最后一个叶子节点下标while (end > 0){Swap(&a[0], &a[end]);// 开始向下调整AdjustDown(a, end, 0);end--;}printf("堆排序结果展示:\n");for (int j = 0; j < n; j++){printf("%d ", a[j]);}
    }int main()
    {// 堆排序测试int a[100];int j = 0;int sum;printf("请输入你要插入堆的数据(-1结束):>\n");for (int i = 0; i < 100; i++){scanf("%d", &sum);if (sum == -1){break;}a[j++] = sum;}HPSort(a, j);return 0;
    }

    ✨:效果演示:

三、共勉

以下就是我对数据结构---堆排序的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对数据结构-------堆的应用TopK问题请持续关注我哦!!!!

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

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

相关文章

竞赛 机器视觉opencv答题卡识别系统

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 答题卡识别系统 - opencv python 图像识别 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分…

RabbitMQ核心总结

AMQP协议核心概念 RabbitMQ是基于AMQP协议的&#xff0c;通过使用通用协议就可以做到在不同语言之间传递。 server&#xff1a;又称broker&#xff0c;接受客户端连接&#xff0c;实现AMQP实体服务。 connection&#xff1a;连接和具体broker网络连接。 channel&#xff1a…

Spring Boot 技术架构图(InsCode AI 创作助手辅助)

Spring Boot 技术架构是一种用于构建现代应用程序的框架&#xff0c;它可以与各种前端、代理、网关、业务服务、中间件、存储、持续集成和容器服务集成在一起&#xff0c;以创建功能强大的应用程序。 源文件下载链接&#xff01;&#xff01;&#xff01;&#xff01;&#xff…

线程概念,实现方式以及多线程模型

1.线程引入 有的进程可能需要“同时”做很多事&#xff0c;而传统的进程只能串行地执行一系列程序。 为此&#xff0c;引入了“线程”&#xff0c;来增加并发度。 可以把线程理解为“轻量级进程”。线程是一个基本的CPU执行单元&#xff0c;也是程序执行流的最小单位。引入线…

鱼眼相机去畸变(图像拉直/展开/矫正)算法及实战总结

本文介绍两种方法 1、经纬度矫正法 2、棋盘格矫正法 一、经纬度矫正法 1、算法说明 经纬度矫正法&#xff0c; 可以把鱼眼图想象成半个地球&#xff0c; 然后将地球展开成地图&#xff0c;经纬度矫正法主要是利用几何原理&#xff0c; 对图像进行展开矫正。 经过P点的入射光线…

手机上记录的备忘录内容怎么分享到电脑上查看?

手机已经成为了我们生活中不可或缺的一部分&#xff0c;我们用它来处理琐碎事务&#xff0c;记录生活点滴&#xff0c;手机备忘录就是我们常用的工具之一。但随着工作的需要&#xff0c;我们往往会遇到一个问题&#xff1a;手机上记录的备忘录内容&#xff0c;如何方便地分享到…

如何在Keil和IAR环境编译生成的bin文件添加CRC校验值

之前写过一篇文章介绍过 CRC 的原理和应用。在程序升级的情况下&#xff0c;我们可以在烧录下载的 bin 文件添加 CRC 校验值&#xff0c;以校验我们获取的bin文件是否正确。 下面我打算使用 APM32F407 的工程代码&#xff0c;介绍下如何在 Keil 环境和 IAR 环境对编译生成的 b…

leetCode 45.跳跃游戏 II 贪心算法

45. 跳跃游戏 II - 力扣&#xff08;LeetCode&#xff09; 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 &…

【算法——双指针】LeetCode 18 四数之和

题目描述&#xff1a; 解题思路&#xff1a;双指针 四数之和与前面三数之和思路一样&#xff0c;排序后&#xff0c;枚举 nums[a]作为第一个数&#xff0c;枚举 nums[b]作为第二个数&#xff0c;那么问题变成找到另外两个数&#xff0c;使得这四个数的和等于 target&#xff0c…

面试题:熟悉设计模式吗?谈谈简单工厂模式和策略模式的区别

刚刚接触设计模式的时候&#xff0c;我相信单例模式和工厂模式应该是用的最多的&#xff0c;毕竟很多的底层代码几乎都用了这些模式。自从接触了一次阿里的公众号发的一次文章关于 DDD的使用 以后&#xff0c;就逐渐接触了策略模式。现在在项目中运用最多的也是这几种设计模式了…

【数据结构初阶】六、线性表中的队列(C语言 -- 链式结构实现队列)

相关代码gitee自取&#xff1a; C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a; 【数据结构初阶】五、线性表中的栈&#xff08;C语言 -- 顺序表实现栈&#xff09;_高高的胖子的博客-CSDN博客 1 . 队列&#xff08;Queue&#xff09; 队列的概念和结构&#xf…

【Linux】文件权限详解

&#x1f341; 博主 "开着拖拉机回家"带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——&#x1f390;开着拖拉机回家_Linux,Java基础学习,大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341; 希望本文能够给您带来一定的…

国庆作业6

TCP服务器 #include "head.h" #define PORT 2580 //端口号 #define IP "192.168.31.219" //本机IP int main(int argc, const char *argv[]) {sqlite3* dbNULL;if(sqlite3_open("./my.db",&db)!SQLITE_OK){fprintf(stde…

Java 随机数的获得方法(5种)

1. Math.random() 静态方法 产生的随机数是 0 - 1 之间的一个 double&#xff0c;即 0 < random < 1 代码&#xff1a; 结果&#xff1a; 当调用 Math.random() 方法时&#xff0c;自动创建了一个伪随机数生成器&#xff0c;实际上用的是 new java.util.Random()。当接…

pwnable_hacknote

pwnable_hacknote Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8047000)32位&#xff0c;没开PIE main部分就不贴了&#xff0c;直接贴主要的函数 unsigned int ADD() {int v0; // ebxint i; // [e…

【Kafka专题】Kafka快速实战以及基本原理详解

目录 前言课程内容一、Kafka介绍1.1 MQ的作用1.2 为什么用Kafka 二、Kafka快速上手2.1 实验环境2.2 单机服务体验2.3 认识Kafka模型架构2.4 Kafka集群2.5 理解服务端的Topic、Partion和Broker2.6 章节总结&#xff1a;Kafka集群的整体结构 三、Kraft集群&#xff08;拓展&#…

【计算机网络】高级IO之select

文章目录 1. 什么是IO&#xff1f;什么是高效 IO? 2. IO的五种模型五种IO模型的概念理解同步IO与异步IO整体理解 3. 阻塞IO4. 非阻塞IOsetnonblock函数为什么非阻塞IO会读取错误&#xff1f;对错误码的进一步判断检测数据没有就绪时&#xff0c;返回做一些其他事情完整代码myt…

Linux和本地Windows如何互传文件(sz和rz指令)

目录 关于 rzsz 注意事项 安装软件 rz的使用&#xff08;本地主机文件传到Windows中&#xff09; sz的使用(Linux中的文件传到本地Windows主机中) 关于 rzsz 这个工具用于 windows 机器和远端的 Linux 机器通过 XShell 传输文件. 安装完毕之后可以通过直接拖拽的方式将文件…

【源码】hamcrest 源码阅读及空对象模式、模板方法模式的应用

文章目录 前言1. 类图概览2. 源码阅读2.1 抽象类 BaseMatcher2.1 接口 Description提炼模式&#xff1a;空对象模式 2. 接口 Description 与 SelfDescribing 配合使用提炼模式 模板方法 后记 前言 hamcrest &#xff0c;一个被多个测试框架依赖的包。听说 hamcrest 的源码质量…

Linux性能优化--性能工具:系统内存

3.0.概述 本章概述了系统级的Linux内存性能工具。本章将讨论这些工具可以测量的内存统计信息&#xff0c;以及如何使用各种工具收集这些统计结果。阅读本章后&#xff0c;你将能够&#xff1a; 理解系统级性能的基本指标&#xff0c;包括内存的使用情况。明白哪些工具可以检索…