当前位置: 首页 > news >正文

排序【归并排序和计数排序】

1.归并排序

1.1 基本思想

并归排序:是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
在这里插入图片描述

1.2 递归法:

也就是说,我们在得到有序列之前,要保证俩个子集有序(左区间和右区间),但子集又要分成俩个小子集来使子集有序,小子集又能分出俩个更小的子集,直到子集无法在分,也就是子集仅剩一个元素。

由于我们不能太早的修改原数组的内容,所以我们需要开辟一块新空间(tmp)来暂时存放内容,等内容全部排好序,再将tmp拷贝给原数组。

由于是递归,如果在开辟空间的函数递归,就会一直开辟空间、释放空间,这样会有效率低下的问题,所以我们用一个子函数来完成排序的部分。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void _MergeSort(int* a, int* tmp, int begin, int end);
//归并排序
void MergeSort(int* a, int n)
{//开辟的空间int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}//排序主体_MergeSort(a, tmp, 0, n-1);free(tmp);tmp = NULL;
}void TestMergeSort()
{int a[] = { 9,8,6,2,3,4,5,1,7,10 };MergeSort(a, sizeof(a) / sizeof(int));PrintArray(a, sizeof(a) / sizeof(int));
}

1.2.1 子函数

void _MergeSort(int* a, int* tmp, int begin, int end);

我们先将数组进行分割,分割点就为中间点(mid),分割出 [ b e g i n , m i d ] [ m i d + 1 , e n d ] [begin, mid][mid+1, end] [begin,mid][mid+1,end],然后当集合只剩下一个元素的时候就跳出递归

void _MergeSort(int* a, int* tmp, int begin, int end)
{if (begin >= end){return;}int mid = (begin + end) / 2;_MergeSort(a, tmp, begin, mid);_MergeSort(a, tmp, mid + 1, end);
}

1.2.2 排序主体

然后我们就来完成排序的主体部分,因为我们是用区间来分割数组,所以我们需要四个变量来记录俩个区间的开始和结束。
除此之外,我们还需要一个下标变量来记录元素的所在位置,那么我们给下标初始化就不能给0,而是要给相对位置begin

我们判断俩个区间哪个值更小(更大),将较小的值存放到tmp数组里,然后让较小的begin++,直到俩个区间中的任意一个区间没有值(begin == end),就跳出循环,然后将有值区间内的剩余值放到tmp数组,最后将tmp数组的值拷贝给原数组。

void _MergeSort(int* a, int* tmp, int begin, int end)
{if (begin >= end){return;}int mid = (begin + end) / 2;_MergeSort(a, tmp, begin, mid);_MergeSort(a, tmp, mid + 1, end);//左区间int begin1 = begin, end1 = mid;//右区间int begin2 = mid+1, end2 = end;//下标int i = begin;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[i++] = a[begin1++];}else{tmp[i++] = a[begin2++];}}while (begin1 <= end1){tmp[i++] = a[begin1++];}while (begin2 <= end2){tmp[i++] = a[begin2++];}//拷贝memcpy(a+begin, tmp+begin, (end - begin + 1) * sizeof(int));
}

这个方法与二叉树的后序遍历很相似,先排序后左区间,在排序右区间,最后在将左右区间结合。

完整代码:

用例:int a1[] = { 9, 8, 6, 2, 11, 3, 4, 5, 1, 10, 7 }; 长度为11

#include<stdio.h>
#include<string.h>
#include<stdlib.h>void PrintArray(int* a, size_t n)
{for (int i = 0; i < n; i++){printf("%d ", a[i]);}printf("\n");
}void _MergeSort(int* a, int* tmp, int begin, int end)
{if (begin >= end){return;}int mid = (begin + end) / 2;_MergeSort(a, tmp, begin, mid);_MergeSort(a, tmp, mid + 1, end);//左区间int begin1 = begin, end1 = mid;//右区间int begin2 = mid+1, end2 = end;//下标int i = begin;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[i++] = a[begin1++];}else{tmp[i++] = a[begin2++];}}while (begin1 <= end1){tmp[i++] = a[begin1++];}while (begin2 <= end2){tmp[i++] = a[begin2++];}//拷贝memcpy(a+begin, tmp+begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}_MergeSort(a, tmp, 0, n-1);free(tmp);tmp = NULL;
}

在这里插入图片描述

1.3 非递归方法:

出发点并不是先排序一个区间再排序另一个区间这样的深度优先遍历(DFS),而是一层一层排序的广度优先遍历(BFS)

类似下图👇:
在这里插入图片描述

1.3.1 初版

由于我们不是递归,所以我们不会用分割,而是用分组,确定每组的元素个数,再进行排序。

我们使用gap来代表每组的个数,然后两组两组的归并,归并一轮后增加gap的数量,直到gap大于大于数组长度,当然也是要将tmp的值拷贝给原数组。

//循环版
void MergeSort2(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}//分组 , gap为每组有多少个元素int gap = 1;while (gap < n){for (int i = 0; i < n; i += gap * 2){//左区间int begin1 = i, end1 = i + gap - 1;//右区间int begin2 = i + gap, end2 = i + gap * 2 - 1;//下标int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[j++] = a[begin1++];}else{tmp[j++] = a[begin2++];}}while (begin1 <= end1){tmp[j++] = a[begin1++];}while (begin2 <= end2){tmp[j++] = a[begin2++];}memcpy(a+i, tmp+i, sizeof(int) * (end2-i+1));}gap *= 2;}free(tmp);tmp = NULL;
}

在这里插入图片描述
可以看到运行成功了,但是,这个排序算法仅仅支持长度为2的次方倍的数组,因为gap是以2的倍数增长,那么当这个数组长度不是2的次方倍的话,就一定会越界访问。

举例:int a2[] = { 9,8,6,2,3,4,5,1,10,7 };
在这里插入图片描述
可以看到报错了

1.3.2 优化

我们先来看看左右区间的下标(原数组的长度是10,最后一个元素的下标为9)
在这里插入图片描述
我们可以看到画红线的就是越界了的
我们可以这样解决:当begin2越界,就代表end2肯定也是越界的,那么这次就不排序,直接退出,如果只有end2越界,那我们就将end2修正成 n − 1 n-1 n1,也就是最后一个元素的下标。
这样可以不需要管end1越界的情况,因为end1越界了,begin2肯定也越界了,就会直接退出。

			//左区间int begin1 = i, end1 = i + gap - 1;//右区间int begin2 = i + gap, end2 = i + gap * 2 - 1;if (begin2 > n){break;}if (end2 > n){end2 = n - 1;}

这样我们就不用管这个数组是不是2的倍数,是不是偶数,因为会将end2修正为n-1,归并并没有规定排序规定两个子集的长度是一样的,所以我们可以把一部分已经排好序但是begin2越界的组留下来,等到最后排序(中途也有可能排序)

完整代码:

用例:int a2[] = { 9,8,6,2,11,3,4,5,1,10,7 }; 长度为11

//循环版
void MergeSort2(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}//分组 , gap为每组有多少个元素int gap = 1;while (gap < n){for (int i = 0; i < n; i += gap * 2){//左区间int begin1 = i, end1 = i + gap - 1;//右区间int begin2 = i + gap, end2 = i + gap * 2 - 1;//printf("[%d][%d] [%d][%d]  ", begin1, end1, begin2, end2);//begin2越界,就代表这个区间不存在if (begin2 >= n){break;}//只有end2越界,修正if (end2 >= n){end2 = n - 1;}//下标int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[j++] = a[begin1++];}else{tmp[j++] = a[begin2++];}}while (begin1 <= end1){tmp[j++] = a[begin1++];}while (begin2 <= end2){tmp[j++] = a[begin2++];}memcpy(a+i, tmp+i, sizeof(int) * (end2-i+1));}gap *= 2;}free(tmp);tmp = NULL;
}

在这里插入图片描述

1.4 特性总结

  1. 归并的缺点就在于需要 O ( N ) O(N) O(N)的空间复杂度,归并排序主要是解决数据在磁盘(固态硬盘)中的外排序问题。
  2. 时间复杂度: O ( N ∗ l o g N ) O(N*logN) O(NlogN)
  3. 空间复杂度: O ( N ) O(N) O(N)
  4. 稳定性:稳定

2. 计数排序

计数排序其实是一种非比较排序,是不需要进行数据之间比较大小的排序。
操作步骤:

  1. 统计相同元素出现的次数
  2. 根据统计的结果,将序列回收到原来的序列中

2.1 操作讲解:

我们既然要统计次数,那么就需要建立一个tmp数组(要将tmp数组里的元素都初始化成0)。

在这里插入图片描述
然后我们遍历一遍a数组,计算a数组下标元素出现的次数,拿a[0]也就是9来举例。
a数组遍历到9的时候,那么tmp数组中下标为9的元素就+1,换成代码就是++tmp[a[i]],将a[i]作为tmp的下标。

在这里插入图片描述
这样依次类推,直到遍历完a数值,然后再遍历一遍tmp数组,将tmp数组中非零值的下标依次拷贝到原数组中。
在这里插入图片描述
但是,我们这里使用的绝对位置,如果这些数是从100开头,然后只排序10个数(101到110),那么我们真正会利用到的也就是一百以后的空间,前100个空间就就浪费了,所以我们要用相对位置。

2.2 相对位置

我们要在上文的遍历a数组开辟tmp前,先遍历一遍a数组,找到其中的最大值和最小值,然后让他们相减再加一,就是我们需要的范围了(int range = max - min + 1;),就拿上文中a数组的0~9来说吧,先遍历一遍,找到min = 0, max = 9,那么我们再根据range的公式得出 range = 10,再用range来开辟tmp数组。

void CountSort(int* a, int n)
{int min = a[0];int max = a[0];//通过遍历来获取max和min的值for (int i = 0; i < n; i++){if (a[i] < min){min = a[i];}if (a[i] > max){max = a[i];}}//计算得出range的值并开空间int range = max - min + 1;int* count = (int*)calloc(range, sizeof(int));if (count == NULL){perror("calloc fail");return;}//排序……
}

然后再进行上文的操作,但到了回收的时候就要注意了,这时就不能是直接让tmp下标赋值给a数组,而是a[i++] = j + min(i为a数组的下标,j为tmp数组的下标)。

2.3 完整代码

用例:int a1[] = { 9, 8, 6, -2, -11, 3, 4, 5, 1, 10, 7,10,7 ,9, 8, 6, 3, 4, 5, 1, 7, 7, 8, 6, -2, -11, 5, 1 };

void CountSort(int* a, int n)
{int min = a[0];int max = a[0];//通过遍历来获取max和min的值for (int i = 0; i < n; i++){if (a[i] < min){min = a[i];}if (a[i] > max){max = a[i];}}//计算得出range的值并开空间int range = max - min + 1;int* count = (int*)calloc(range, sizeof(int));if (count == NULL){perror("calloc fail");return;}//排序主体//计数for (int i = 0; i < n; i++){count[a[i] - min]++;}//回收int j = 0;for (int i = 0; i < range; i++){while (count[i]){a[j++] = i + min;--count[i];}}free(count);count = NULL;
}

在这里插入图片描述

可以看到这个计数排序的思想也是可以解决负数的问题。

2.4 特性总结

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  2. 时间复杂度: O ( M A X ( N , r a n g e ) ) O(MAX(N,range)) O(MAX(N,range))
  3. 空间复杂度: O ( r a n g e ) O(range) O(range)
  4. 稳定性:稳定

3.总结

本文主要讲解了归并排序和计数,归并排序讲解了递归与非递归的版本,递归版本是采用了二叉树遍历中后序遍历的思想(先遍历左右孩子,再遍历自己),而非递归的重点是对于区间的掌控。

计数排序其实是非比较排序的一种,还有俩个比较出名的非比较排序(基数排序和桶排序),由于逻辑复杂且几乎没有实践意义,在本文就没有讲解,计数排序的重点在于开辟空间的时候要使用相对位置,且回收时是用tmp下标加min来回收。

最后感谢您能阅读完此片文章,如果有任何建议或纠正欢迎在评论区留言,也可以前往我的主页看更多好文哦(点击此处跳转到主页)。
如果您认为这篇文章对您有所收获,点一个小小的赞就是我创作的巨大动力,谢谢!!!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【图像去雾系列】使用暗通道先验去雾算法对图像进行去雾处理
  • 刷题记录第109天-K个一组反转链表
  • 你知道AI模型是如何学习的吗?
  • keepalived安装-集群部署
  • 【面试题】接雨水
  • WPF 数据模板DataTemplate、控件模板ControlTemplate、Style、ItemsPreseter
  • jenkins 安装以及自动构建maven项目并且运行
  • thinkphp漏洞之sql注入漏洞-builder处漏洞
  • wiota窄带通讯技术对于vu传统lora
  • [cvpr 2024 目标检测 前沿研究 热点] cpvr 2024中与目标检测主题有关的论文
  • 【无标题】第九章 从 Web 客户端指定自定义传输
  • 钢板现货:A572Gr60和SA572Gr60材质分析、A572Gr60和SA572Gr60简介
  • fetch 在实际项目中的思考
  • 2024华为OD机试真题- 求字符串中所有整数的最小和-(C++/Python)-C卷D卷-100分
  • 中国联通在国外最大的综合电信服务枢纽
  • 【Amaple教程】5. 插件
  • 03Go 类型总结
  • co模块的前端实现
  • css布局,左右固定中间自适应实现
  • JavaScript 基础知识 - 入门篇(一)
  • JavaScript/HTML5图表开发工具JavaScript Charts v3.19.6发布【附下载】
  • Perseus-BERT——业内性能极致优化的BERT训练方案
  • PHP 使用 Swoole - TaskWorker 实现异步操作 Mysql
  • Puppeteer:浏览器控制器
  • React as a UI Runtime(五、列表)
  • 记一次删除Git记录中的大文件的过程
  • 前端
  • 数据库写操作弃用“SELECT ... FOR UPDATE”解决方案
  • 微信支付JSAPI,实测!终极方案
  • 云栖大讲堂Java基础入门(三)- 阿里巴巴Java开发手册介绍
  • 智能合约开发环境搭建及Hello World合约
  • ​决定德拉瓦州地区版图的关键历史事件
  • ​数据链路层——流量控制可靠传输机制 ​
  • $emit传递多个参数_PPC和MIPS指令集下二进制代码中函数参数个数的识别方法
  • (1)Hilt的基本概念和使用
  • (delphi11最新学习资料) Object Pascal 学习笔记---第8章第5节(封闭类和Final方法)
  • (echarts)echarts使用时重新加载数据之前的数据存留在图上的问题
  • (Forward) Music Player: From UI Proposal to Code
  • (Java实习生)每日10道面试题打卡——JavaWeb篇
  • (JS基础)String 类型
  • (离散数学)逻辑连接词
  • (免费分享)基于springboot,vue疗养中心管理系统
  • (详细文档!)javaswing图书管理系统+mysql数据库
  • (转)视频码率,帧率和分辨率的联系与区别
  • *算法训练(leetcode)第三十九天 | 115. 不同的子序列、583. 两个字符串的删除操作、72. 编辑距离
  • .[hudsonL@cock.li].mkp勒索加密数据库完美恢复---惜分飞
  • .【机器学习】隐马尔可夫模型(Hidden Markov Model,HMM)
  • .net core webapi 部署iis_一键部署VS插件:让.NET开发者更幸福
  • .NET Core 网络数据采集 -- 使用AngleSharp做html解析
  • .Net Web窗口页属性
  • .NET 某和OA办公系统全局绕过漏洞分析
  • .NET/C# 使用反射注册事件
  • .Net程序帮助文档制作
  • .net获取当前url各种属性(文件名、参数、域名 等)的方法
  • .Net下的签名与混淆