排序(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

相关文章

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

MySQL中的表连接原理分析

《MySQL中的表连接原理分析》:本文主要介绍MySQL中的表连接原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、环境3、表连接原理【1】驱动表和被驱动表【2】内连接【3】外连接【4编程】嵌套循环连接【5】join buffer4、总结1、背景

python中Hash使用场景分析

《python中Hash使用场景分析》Python的hash()函数用于获取对象哈希值,常用于字典和集合,不可变类型可哈希,可变类型不可,常见算法包括除法、乘法、平方取中和随机数哈希,各有优缺点,需根... 目录python中的 Hash除法哈希算法乘法哈希算法平方取中法随机数哈希算法小结在Python中,

一文详解Java Stream的sorted自定义排序

《一文详解JavaStream的sorted自定义排序》Javastream中的sorted方法是用于对流中的元素进行排序的方法,它可以接受一个comparator参数,用于指定排序规则,sorte... 目录一、sorted 操作的基础原理二、自定义排序的实现方式1. Comparator 接口的 Lam

Java Stream的distinct去重原理分析

《JavaStream的distinct去重原理分析》Javastream中的distinct方法用于去除流中的重复元素,它返回一个包含过滤后唯一元素的新流,该方法会根据元素的hashcode和eq... 目录一、distinct 的基础用法与核心特性二、distinct 的底层实现原理1. 顺序流中的去重

关于MyISAM和InnoDB对比分析

《关于MyISAM和InnoDB对比分析》:本文主要介绍关于MyISAM和InnoDB对比分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录开篇:从交通规则看存储引擎选择理解存储引擎的基本概念技术原理对比1. 事务支持:ACID的守护者2. 锁机制:并发控制的艺

MyBatis Plus 中 update_time 字段自动填充失效的原因分析及解决方案(最新整理)

《MyBatisPlus中update_time字段自动填充失效的原因分析及解决方案(最新整理)》在使用MyBatisPlus时,通常我们会在数据库表中设置create_time和update... 目录前言一、问题现象二、原因分析三、总结:常见原因与解决方法对照表四、推荐写法前言在使用 MyBATis

Python主动抛出异常的各种用法和场景分析

《Python主动抛出异常的各种用法和场景分析》在Python中,我们不仅可以捕获和处理异常,还可以主动抛出异常,也就是以类的方式自定义错误的类型和提示信息,这在编程中非常有用,下面我将详细解释主动抛... 目录一、为什么要主动抛出异常?二、基本语法:raise关键字基本示例三、raise的多种用法1. 抛

github打不开的问题分析及解决

《github打不开的问题分析及解决》:本文主要介绍github打不开的问题分析及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、找到github.com域名解析的ip地址二、找到github.global.ssl.fastly.net网址解析的ip地址三