排序(3)【归并排序】【计数排序】【排序算法度及其稳定性分析】

2024-06-19 14:44

本文主要是介绍排序(3)【归并排序】【计数排序】【排序算法度及其稳定性分析】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一.归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。利用动图更清晰的看一下过程:

在八个数中进行排序就是这样的: 

 1.归并排序的实现

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;//[begin,mid]与[mid+1,end]int begin2 = mid + 1, end2 = end;int i = begin;//从begin开始while (begin1 <= end1 && begin2 <= end2)//左右区间的值依次开始比较,比较小的那个放到新的区间,下标为i{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++];}//这里因为我们单独创建了一个数组,所以我们需要把tmp里面的值拷贝到原来的数组里memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));//因为我们的区间不一定从哪里开始(begin不确定),我们需要知道我们要从哪里开始拷贝,所以这里加了一个begin
}

这个代码的思想就是,我们把一个区间通过一半一半的分,分成更小的区间,然后对这小区间进行排序,再把小区间和小区间合成一个大区间,其中的每一个小区间都是有序的。这就像是一个二叉树后序的过程。

2.用非递归实现归并排序

上面对我们都是使用递归来实现排序,当然肯定有人不喜欢递归来实现。这里还有一种非递归的方式:

void MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);//动态创建一个数组if (tmp == NULL){perror("tmp::malloc");return;}int gap = 1;//gap指的是每次归并几个元素while (gap < n)//gap不能大于等于n{for (int i = 0; i < n; i += 2 * gap)//这里的循环是把数组里的元素全部进行归并,每组数据个数是gap,还要注意i += 2 * gap指的是跳到下一组{//然后开始分区间[begin1,end1][begin2,end2]//归并是两组两组进行的这一个区间里的值与下一个区间里的值进行归并int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;//这里需要判断三种特殊情况,后面单独说if (begin2 >= n){break;}if (end2 >= n){end2 = n - 1;}//为了不影响变量i,再创建一个变量,往tmp数组里入数据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++];}//因为我们创建的是临时变量tmp,所以这里用memcpy//每次排完两组,我们就拷贝数据,如果在for循环外部,会导致一些错误,后面说memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));}//到这里我们就已经完全的排完序了,下面就是继续增大区间,继续循环归并gap = gap * 2;}//还有就是别忘了释放free(tmp);tmp = NULL;
}

这里还有一些问题要说一下就是上面没有解释的那个代码:

if (begin2 >= n)
{break;
}
if (end2 >= n)
{end2 = n - 1;
}

如果我不加上面的两个if语句的话,我们会发现只有当我们的数据个数是2的n次方的时候才可以完成排序的功能。如果我有十个数的话,它就会有越界的风险。因为我每一次跳过的距离是gap,而gap是呈2的倍数增长。那么后面我在求区间的时候就会出现越界的问题。

有三种情况:

分别代表着end2越界,begin2和end2越界,end1,begin2和end2越界。 后面两种对应着第一个if语句:

if (begin2 >= n)
{break;
}

意思就是,如果我的begin2越界了,我就跳出这个循环,不再进行归并,因为后面的元素个数已经没有了,不需要进行归并,仅需完成前面归并的过程就行了。

然后就是第一种情况,我们只需要调整一下end2的值就行了。原本end2是越界的,我们直接让它成为最后一个元素就行了(end1之前也就是end2,在上一个过程中已经调整过了)。

如果将memcpy放在for循环外部,那么在第一次循环后就会把整个排序结果拷贝到原始数组中,导致后续的归并排序无法正确进行。比如我有11个数,那么在刚开始的时候,我有一个数就不能被拷贝进去,tmp里是10个值,此时我把tmp数组拷贝到原数组里就会出现出错。

3.归并排序的时间复杂度

归并排序的时间复杂度是O(nlogn),其中n是要排序的元素个数。归并排序的核心操作是将待排序的数组不断地分割成两个子数组,直到每个子数组只有一个元素。然后,将两个子数组合并成一个有序数组。这个分割和合并的过程需要重复logn次,每次操作的时间复杂度是O(n)。因此,归并排序的总时间复杂度是O(nlogn)。

空间复杂度是O(n)。

二.计数排序

1.计数排序的实现

这个排序是比较特殊一点的排序,它是不用比较大小就可以把数据排好的一种方式,它所用的方式是统计。

void CountSort(int* a, int n)
{//先找到数组里的最大值和最小值,求出数组里数据的范围int min = a[0];int max = a[0];for (int i = 1; i < n; i++){if (a[i] < min)min = a[i];if (a[i] > max)max = a[i];}int range = max - min + 1;//根据范围的大小开辟空间,注意这里用的calloc直接也可以把开辟的空间里的值初始化为0int* count = (int*)calloc(range, sizeof(int));if (count == NULL){perror("calloc fail");return;}//因为我们count数组里的值都是0,根据相对值,如果出现与count数组下标相同的值我们就加一for (int i = 0; i < n; i++){count[a[i] - min]++;}//到这里就是排序的过程,根据count数组的下标,我们把它的下标值加上min就是我们需要排序的数,依次放进数组里int j = 0;for (int i = 0; i < range; i++){while (count[i]--){a[j++] = i + min;}}free(count);
}

可能稍微用了一点比较大小的方式,但是主体我们利用了下标统计的方式来实现排序。

这个排序逻辑没有那么复杂,效率也很快,但是唯一不足的是它只能排列整数,遇到小数,负数,结构体等等都不行了。

 2.计数排序的时间复杂度

计数排序的时间复杂度为O(n+k),其中n为待排序元素的个数,k为待排序元素的取值范围。在最坏情况下,计数排序的时间复杂度可以达到O(n^2),但是当k不大时,计数排序的时间复杂度一般是线性的,即O(n)。相比于其他排序算法,计数排序的时间复杂度较低,但是需要额外的空间来存储计数数组。

三.排序算法度及其稳定性分析

 注意这里的稳定性指的是相同的数相对位置不变。

到这里一些常见的排序算法就完全结束了。感谢大家的观看,如有错误还请多多指出。

这篇关于排序(3)【归并排序】【计数排序】【排序算法度及其稳定性分析】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1075322

相关文章

Java Map排序如何按照值按照键排序

《JavaMap排序如何按照值按照键排序》该文章主要介绍Java中三种Map(HashMap、LinkedHashMap、TreeMap)的默认排序行为及实现按键排序和按值排序的方法,每种方法结合实... 目录一、先理清 3 种 Map 的默认排序行为二、按「键」排序的实现方式1. 方式 1:用 TreeM

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

深入理解Mysql OnlineDDL的算法

《深入理解MysqlOnlineDDL的算法》本文主要介绍了讲解MysqlOnlineDDL的算法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小... 目录一、Online DDL 是什么?二、Online DDL 的三种主要算法2.1COPY(复制法)

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

Python内存管理机制之垃圾回收与引用计数操作全过程

《Python内存管理机制之垃圾回收与引用计数操作全过程》SQLAlchemy是Python中最流行的ORM(对象关系映射)框架之一,它提供了高效且灵活的数据库操作方式,本文将介绍如何使用SQLAlc... 目录安装核心概念连接数据库定义数据模型创建数据库表基本CRUD操作创建数据读取数据更新数据删除数据查

MyBatis Plus大数据量查询慢原因分析及解决

《MyBatisPlus大数据量查询慢原因分析及解决》大数据量查询慢常因全表扫描、分页不当、索引缺失、内存占用高及ORM开销,优化措施包括分页查询、流式读取、SQL优化、批处理、多数据源、结果集二次... 目录大数据量查询慢的常见原因优化方案高级方案配置调优监控与诊断总结大数据量查询慢的常见原因MyBAT

分析 Java Stream 的 peek使用实践与副作用处理方案

《分析JavaStream的peek使用实践与副作用处理方案》StreamAPI的peek操作是中间操作,用于观察元素但不终止流,其副作用风险包括线程安全、顺序混乱及性能问题,合理使用场景有限... 目录一、peek 操作的本质:有状态的中间操作二、副作用的定义与风险场景1. 并行流下的线程安全问题2. 顺

MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决

《MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决》MyBatis默认开启一级缓存,同一事务中循环调用查询方法时会重复使用缓存数据,导致获取的序列主键值均为1,... 目录问题原因解决办法如果是存储过程总结问题myBATis有如下代码获取序列作为主键IdMappe

Java中最全最基础的IO流概述和简介案例分析

《Java中最全最基础的IO流概述和简介案例分析》JavaIO流用于程序与外部设备的数据交互,分为字节流(InputStream/OutputStream)和字符流(Reader/Writer),处理... 目录IO流简介IO是什么应用场景IO流的分类流的超类类型字节文件流应用简介核心API文件输出流应用文