【初阶数据结构与算法】八大排序算法之归并排序与非比较排序(计数排序)

在这里插入图片描述

文章目录

  • 一、归并排序
  • 二、非比较排序之计数排序
  • 三、归并排序和计数排序的性能测试

一、归并排序

   归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide andConquer)的⼀个⾮常典型的应⽤
   大致就是将已有序的⼦序列合并,得到完全有序的序列,即先使每个⼦序列有序,再使⼦序列段间有序,若将两个有序表合并成⼀个有序表,称为⼆路归并
   通俗一点说,就是将一个数组无脑递归二分,一直二分到只剩下一个元素,然后开始回归,在回归的时候,将元素进行排序,类似于将两个有序数组合并为一个数组的过程,我们画个图演示一下:
在这里插入图片描述
   整个归并排序就是如图所示的分解以及合并的过程,先将数组不断二分,直到只剩一个元素,然后开始合并,每一次合并都是将两个有序序列进行合并,所以合并不会很难
   那么接下来我们就先来学习如何分解,其实就是利用递归,进行划分,和我们二叉树的后序遍历有点相似,将整个过程看作二叉树的后序遍历,先将给出的数,组划分成左右两部分,然后再将左右继续划分成两部分,这就相当于递归左右子树,这就是分解的过程
   然后就是对分解好的序列进行合并,我们可以发现,每一次合并时都是对有序序列进行合并,如果只有单个元素的话,也可以看作有序,两个单独的元素合并后,又变成了有序的序列,如上面演示的图,所以我们的合并其实就是对两组有序序列进行合并
   但是我们考虑到,在合并时不方便直接对原数组进行调整,所以我们可以重新开一个和原数组大小相同的数组tmp,用来暂时存放我们合并后的数据,当每一次子合并完成后就将tmp中的数据拷贝回原数组
   于是我们又发现,如果直接对当前的函数进行递归,那么递归多少次,就要开辟多少个tmp数组,非常不值得,所以我们可以在函数中创建一个tmp数组,然后创建一个子函数来递归解决问题,如下:

//归并排序
void MergeSort(int* arr, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc");return;}//创建子函数用于递归调用(分解、合并)_MergeSort(arr, 0, n - 1, tmp);free(tmp);tmp = NULL;
}

   上面就是我们归并排序的大致框架,其中子函数需要区间范围的下标方便我们进行分解和合并,接下来我们就按照之前的思路来设计子函数,其中合并的过程就是将两个有序序列合并成一个有序序列,之前在顺序表刷题那里讲过这道题,这里就不再多说,直接写代码,如下:

void _MergeSort(int* arr, int left, int right, int* tmp)
{//left = right说明这个区间只有一个元素,直接返回,无需处理if (left >= right){return;}//算出中间值方便将当前序列二分int mid = left + (right - left) / 2;//二分后的左序列范围[left, mid],右序列范围:[mid + 1, right]//接着根据两个序列的范围继续二分_MergeSort(arr, left, mid, tmp);_MergeSort(arr, mid + 1, right, tmp);//上面就是分解的步骤,分解到最后我们进行合并操作//其实就是将两个有序数组合并到另一个数组,并且保持有序的那个题的做法//记录第一个有序序列的开始和结束下标int begin1 = left, end1 = mid;//记录第二个有序序列的开始和结束下标int begin2 = mid + 1, end2 = right;//由于不方便对arr数组直接进行原地操作//所以接下来将它们合并到tmp数组的对应位置上int index = begin1;while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] < arr[begin2]){tmp[index++] = arr[begin1++];}else{tmp[index++] = arr[begin2++];}}//走到这里说明有一个序列已经放完了,接着将另一个序列按顺序放入tmp数组中while (begin1 <= end1){tmp[index++] = arr[begin1++];}while (begin2 <= end2){tmp[index++] = arr[begin2++];}//现在将两个有序序列合并为一个有序序列存放在tmp后//将tmp中的数据重新拷贝回arrfor (int i = left; i <= right; i++){arr[i] = tmp[i];}
}

这就是完整的归并排序了,接着我们来测试一下,如图:
在这里插入图片描述
   可以看到归并排序成功帮组我们完成了排序任务,接着我们来分析一下它的时间复杂度,首先递归需要log N次,每一次递归按最坏情况计算就是O(N),所以它的时间复杂度大致就是O(N * log N)
   接着由于它在排序时需要申请和原数组一样大小的空间来辅助排序,所以它的空间复杂度为O(N)

二、非比较排序之计数排序

   在我们之前学习的排序算法中,无论怎样都需要比较两个数的大小,随后进行排序,而计数排序则非常巧妙,无需任何两个数之间的比较就可以对数组排序,接着我们来认识一下它
   计数排序是对哈希直接定址法的变形应⽤,如果了解过哈希表那么这个排序就是小菜一碟,但是为了能给大家讲清楚,我们先画画图,来看看计数排序的运作过程,不求看懂,只求留点印象,然后我们再来讲思路,否则直接讲思路肯定会绕进去,如图:
在这里插入图片描述
在这里插入图片描述
   我们可以看到,如果我们能够实现上述操作,将所有元素映射到一个数组的下标上,然后从这个数组中重新取出这些元素,就能将原数组中的元素排成升序,这个过程中没有对任何元素进行比较,依赖的就是数组的下标有序,是不是非常巧妙呢?
   但是其实上面画的图的思路还可以优化一下,避免一些特殊情况,接下来我们就来讲讲真正的计数排序的大概思路,随后我们按照这个思路一点一点来实现出这个计数排序,大致思路就是:

  1. 预处理一个count数组,默认count数组里面全部存放0,将待排序的数组的元素映射到count数组的下标元素上,比如一个元素3就放入count数组下标为3的位置,放入的方法就是让count[3]++,这就代表了3元素出现了一次
  2. 当我们按照第1步将所有元素放入count数组后,我们再遍历count数组,将里面的元素重新放入原数组中,即可排成有序,利用的就是数组下标有序
  3. 如果最小的元素都特别大,可能浪费空间,比如只有两个数1001和1000,count数组就要开辟1002个空间,这样才能创造出0到1001的下标,将1001和1000放进去,所以可以进行一下优化,开原数组最大值 - 最小值 + 1个空间大小的数组,然后放的时候就让每个元素都减去最小值即可

   以上是计数排序的大致思路,接着我们来按照上面的思路进行具体实现的分析,剖析它是怎么做到的,首先是第一步,我们要对count数组的空间进行优化,所以要先找出最大值和最小值,如下:

int min = arr[0], max = arr[0];
for (int i = 1; i < n; i++)
{if (arr[i] < min)min = arr[i];if (arr[i] > max)max = arr[i];
}

   这个很简单就不再多说,接下来我们来开辟count数组,首先我们要根据最大值和最小值来算出count数组的具体大小,方法就是max - min + 1,可能会对这个+1比较迷惑,我们举一个例子就知道了,比如以下数组:
在这里插入图片描述
   如果我们直接用max - min,得到的结果就是17,开完count数组后,它的下标范围为0 - 16,我们要将每个数减去最小值992然后映射到count数组,其它的元素都没有问题,当我们放最大元素1009时,减去最小值992等于17,说明最大值要映射到下标17的位置
   但是我们刚刚开的数组的大小是17,最大下标也才16,也就导致了越界访问,所以我们在开空间的时候要多开一个空间,防止存放最大值时出现越界行为
   接着开空间的时候我们也要注意,需要将数组的每一个元素都初始化成0,因为我们将元素映射到count数组时,要让对应元素位置的值++,如图:
在这里插入图片描述
   所以根据上图的原因,如果我们不将数组初始化为0,里面是随机值就会导致后续操作出错,其中有两种方法,一种方法就是使用calloc开辟count数组,这样count数组一开辟出来里面的值就被初始化成了0
   第二种方法就是使用malloc开辟count数组,然后使用memset函数将所有元素初始化为0,这里为了方便,我们可以直接使用calloc函数开辟数组,如下:

int range = max - min + 1;
int* count = (int*)calloc(sizeof(int), range);
if (count == NULL)
{perror("calloc");return;
}

   现成我们已经将count数组开好了,接下来就是遍历待排序数组arr,将其中的元素映射到count数组中,如下:

for (int i = 0; i < n; i++)
{//将arr[i] - min映射到count数组中//在恢复时加上最小值min即可count[arr[i] - min]++;
}

   我们将所有元素映射到count数组后,这些元素已经按照下标排好序了,不需要我们去比较,这就是非比较排序,那么接下来我们就遍历整个count数组,将它按顺序把数据还原到原数组,如下:

int j = 0;
for (int i = 0; i < range; i++)
{
//这样写的原因就是如果一个元素出现多次,那么count[i]就大于1
//我们要把所有元素取出,所以这样写while (count[i]--){//下标+min就能将元素还原arr[j++] = i + min;}
}

这样我们的计数排序就完成了,我们来看看完成代码:

//计数排序
void CountSort(int* arr, int n)
{//定义min和max去找最小和最大值int max = arr[0], min = arr[0];for (int i = 1; i < n; i++){if (arr[i] < min)min = arr[i];if (arr[i] > max)max = arr[i];}//求出count数组的大小,以及开辟count数组int range = max - min + 1;//注意,如果不用calloc的话,要使用memset将所有元素初始化为0int* count = (int*)calloc(sizeof(int), range);if (count == NULL){perror("calloc");return;}//将arr中的数据映射到count数组中for (int i = 0; i < n; i++){count[arr[i] - min]++;}int j = 0;//按照下标顺序依次将count中的数据取回arr数组for (int i = 0; i < range; i++){while (count[i]--){arr[j++] = i + min;}}free(count);count = NULL;
}

我们来测试一下计数排序:
在这里插入图片描述
   可以看到计数排序很好地完成了排序任务,接着我们来分析分析计数排序的时间复杂度和空间复杂度,以及它的一些缺陷
   首先由于只需要遍历原数组一次将元素映射到count数组,然后再取出来,所以时间复杂度为O(N),由于需要开辟count数组,并且count数组的大小是根据数组元素之间的差值来决定,所以空间复杂度为O(N + Range)
   它的缺陷就是:只能排序整数,包括了负数,因为是通过最大和最小值之间的差值来控制映射关系,所以负数也可以映射到count数组中,这个是没有问题的,但是如果要排序浮点数,那么计数排序做不到
   其次,计数排序不适用于最大值和最小值差距过大的场景,比如数组[3, 100001, 323, 23],虽然只有4个元素,但是最大值和最小值差距差不多10万,这样我们使用计数排序就会造成空间浪费,这就是计数排序的一些缺陷,但是同时它是很快的,我们后面测试就知道了

三、归并排序和计数排序的性能测试

   在前面我们介绍了归并排序和计数排序,其中归并排序的时间复杂度为O(N * log N),计数排序的时间复杂度为O(N),我们来简单对比一下它们在同一设备上的表现,还是老样子,写一个排序函数,如下:

void TestOP()
{srand((unsigned int)time(NULL));const int N = 100000;int* a1 = (int*)malloc(sizeof(int) * N);int* a2 = (int*)malloc(sizeof(int) * N);for (int i = 0; i < N; ++i){a1[i] = rand();a2[i] = a1[i];}int begin1 = clock();MergeSort(a1, N);int end1 = clock();int begin2 = clock();CountSort(a2, N);int end2 = clock();printf("MergeSort:%d\n", end1 - begin1);printf("CountSort:%d\n", end2 - begin2);free(a1);free(a2);
}int main()
{TestOP();return 0;
}

   接着我们来运行一下代码,看看归并排序和计数排序排10万个随机数的速度,记得将VS切换到Release版本,以免不准确,如下:
在这里插入图片描述
   可以看到它们的表现都特别好,其中计数排序排10万个随机数更是1毫秒都没有用到,而归并排序4毫秒也不差,这就是O(N * log N )和O(N)的力量,如果感兴趣,可以自行测试更大的数据,这里我就不演示了

   那么今天的排序算法就介绍到这里啦,八大排序算法基本上都已经介绍完了,接下来我们再来一篇讲解非递归版快排和归并排序就可以结束初阶数据结构与算法阶段,到达C++阶段了,敬请期待吧!
   bye~

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

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

相关文章

window安装TradingView

目录 下载安装包 修改文件后缀&#xff0c;解压 将K线换成国内涨红跌绿样式 下载安装包 https://www.tradingview.com/desktop/ 下载完成后是.msix格式文件 &#xff08;我在win10和win11的系统中尝试运行msix都没有成功&#xff0c;所以放弃直接双击运行msix&#xff…

FPGA多路MIPI转FPD-Link视频缩放拼接显示,基于IMX327+FPD953架构,提供2套工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐本博主所有FPGA工程项目-->汇总目录我这里已有的 MIPI 编解码方案我这里已有的FPGA图像缩放方案本博已有的已有的FPGA视频拼接叠加融合方案 3、本 MIPI CSI-RX IP 介绍4、详细设计方案设计原理框图IMX327 及其配置FPD-Link视频…

React+Vite从零搭建项目及配置详解

相信很多React初学者第一次搭建自己的项目&#xff0c;搭建时会无从下手&#xff0c;本篇适合快速实现功能&#xff0c;熟悉React项目搭建流程。 目录 一、创建项目react-item 二、调整项目目录结构 三、使用scss预处理器 四、组件库Ant Design 五、配置基础路由 六、配置…

Unity复刻胡闹厨房复盘 模块一 新输入系统订阅链与重绑定

本文仅作学习交流&#xff0c;不做任何商业用途 郑重感谢siki老师的汉化教程与代码猴的免费教程以及搬运烤肉的小伙伴 版本&#xff1a;Unity6 模板&#xff1a;3D 核心 渲染管线&#xff1a;URP ------------------------------…

从零开始的编程-java篇1.6.1 万变不离其宗,hello word

前言&#xff1a; 通过实践而发现真理&#xff0c;又通过实践而证实真理和发展真理。从感性认识而能动地发展到理性认识&#xff0c;又从理性认识而能动地指导革命实践&#xff0c;改造主观世界和客观世界。实践、认识、再实践、再认识&#xff0c;这种形式&#xff0c;循环往…

【漏洞复现】CVE-2021-45788 SQL Injection

漏洞信息 NVD - cve-2021-45788 Time-based SQL Injection vulnerabilities were found in Metersphere v1.15.4 via the “orders” parameter. Authenticated users can control the parameters in the “order by” statement, which causing SQL injection. API: /test…

Mac系统下 idea运行maven项目中存在的问题BeanDefinitionStoreException

1.在进行 注解XML 方式整合三层架构事出现此问题 org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [/Volumes/PS2000/Java/SpringProject/micro-shop/spring-annotation-practice-03/target/classes/com/ja…

shiro注入filter内存马(绕过长度限制)

shiro环境 https://github.com/yyhuni/shiroMemshell&#xff08;实验环境&#xff09; 这里用的 Client_memshell.java package com.example.demo;import javassist.ClassPool; import javassist.CtClass; import org.apache.shiro.crypto.AesCipherService; import org.ap…

c语言大一期末复习

l例1&#xff1a;输入一行字符&#xff0c;统计其中空格的个数 #include<stdio.h> int main( ) { char ch; int count0; while((chgetchar())!\n) { if(ch ) { count1; } } printf("%d\n",coun…

心血管疾病中医怎么调理

心血管疾病在中医范畴内属于胸痹&#xff0c;中医会根据不同的证候&#xff0c;如心血瘀阻证、寒凝心脉证、痰浊闭阻证、心肾阴虚证、气阴两虚证等&#xff0c;采取不同的调理方法。以下是一些中医调理心血管疾病的常用手段&#xff1a; 一、中药调理 ‌心血瘀阻证‌&#xf…

群晖利用acme.sh自动申请证书并且自动重载证书的问题解决

前言 21年的时候写了一个在群晖&#xff08;黑群晖&#xff09;下利用acme.sh自动申请Let‘s Encrypt的脚本工具 群晖使用acme自动申请Let‘s Encrypt证书脚本&#xff0c;自动申请虽然解决了&#xff0c;但是自动重载一直是一个问题&#xff0c;本人也懒&#xff0c;一想到去…

raid 状态查看 storcli64

场景 当磁盘报错的时候使用该命令排查 fdisk -l /dev/sdb fdisk: cannot open /dev/sdb: Input/output error进一步使用 smartctl 排查 smartctl -a /dev/sdb 输出 smartctl 7.1 2019-12-30 r5022 [x86_64-linux-5.4.0-144-generic] (local build) Copyright (C) 2002-19, B…

《探索PyTorch计算机视觉:原理、应用与实践》

一、PyTorch 与计算机视觉的奇妙相遇 在当今数字化的时代&#xff0c;计算机视觉作为一门能够赋予机器 “看” 的能力的技术&#xff0c;正以前所未有的速度蓬勃发展&#xff0c;深刻地改变着我们的生活和众多行业的运作模式。从智能手机中的人脸识别解锁&#xff0c;到安防监控…

使用VSCode Debugger 调试 React项目

一般我们调试代码时&#xff0c;用的最多的应该就是console.log方式了&#xff0c;还有的是使用Chrome DevTools 通过在对应的 sourcemap代码位置打断点进行调试&#xff0c;除了上面两种方式外还有一种更好用的调试方式&#xff1a; VSCode Debugger。 VSCode Debugger可以直…

mapbox基础,加载mapbox官方地图

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;mapbox 从入门到精通 文章目录 一、&#x1f340;前言1.1 ☘️mapboxgl.Map 地图对象…

汽车IVI中控开发入门及进阶(三十八):手机投屏HiCar开发

手机投屏轻松实现手机与汽车的无缝连接,导航、音乐、通话等功能应有尽有,还支持更多第三方应用,让车载互联生活更加丰富多彩。 HiCar在兼容性和开放性上更具优势。 手机投屏可以说是车机的杀手级应用,大大拓宽了车机的可用性范围。其中华为推出的HiCar就是非常好用的一种。…

Elasticsearch:确保业务规则与语义搜索无缝协作

作者&#xff1a;来自 Elastic Kathleen DeRusso 利用查询规则与语义搜索和重新排序相结合的强大功能。 更多阅读&#xff1a; Elasticsearch 8.10 中引入查询规则 - query rules Elasticsearch 查询规则现已正式发布 - query rules 你是否知道查询规则&#xff08;query ru…

把riscv32位系统弄懂1:riscv32 CPU指令学习

Riscv手册 首先下载手册&#xff1a;文件下载----中国开放指令生态(RISC-V)联盟 从这个页面下载riscv-spec-v2.1中文版 也可以下载中科大的这本&#xff1a;RISC-V手册 Riscv32指令集包括基础指令集和一些扩展指令集&#xff0c;比如在ESP32C3技术手册中&#xff0c;写到E…

全国消费水平系统|Java|SSM|JSP|

【技术栈】 1⃣️&#xff1a;架构: B/S、MVC 2⃣️&#xff1a;系统环境&#xff1a;Windowsh/Mac 3⃣️&#xff1a;开发环境&#xff1a;IDEA、JDK1.8、Maven、Mysql5.7 4⃣️&#xff1a;技术栈&#xff1a;Java、Mysql、SSM、Mybatis-Plus、JSP、jquery,html 5⃣️数据库可…

达梦数据库-读写分离集群部署

读写分离集群部署 读写分离集群由一个主库以及一个或者多个(最多可以配置 8 个)实时备库组成&#xff0c;基于实时归档实现的高性能数据库集群&#xff0c;不但提供数据保护、容灾等数据守护基本功能&#xff0c;还具有读写操作自动分离、负载均衡等特性。同时可以配置确认监视…