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

相关文章

Android 缓存日志Logcat导出与分析最佳实践

《Android缓存日志Logcat导出与分析最佳实践》本文全面介绍AndroidLogcat缓存日志的导出与分析方法,涵盖按进程、缓冲区类型及日志级别过滤,自动化工具使用,常见问题解决方案和最佳实... 目录android 缓存日志(Logcat)导出与分析全攻略为什么要导出缓存日志?按需过滤导出1. 按

C++归并排序代码实现示例代码

《C++归并排序代码实现示例代码》归并排序将待排序数组分成两个子数组,分别对这两个子数组进行排序,然后将排序好的子数组合并,得到排序后的数组,:本文主要介绍C++归并排序代码实现的相关资料,需要的... 目录1 算法核心思想2 代码实现3 算法时间复杂度1 算法核心思想归并排序是一种高效的排序方式,需要用

Linux中的HTTPS协议原理分析

《Linux中的HTTPS协议原理分析》文章解释了HTTPS的必要性:HTTP明文传输易被篡改和劫持,HTTPS通过非对称加密协商对称密钥、CA证书认证和混合加密机制,有效防范中间人攻击,保障通信安全... 目录一、什么是加密和解密?二、为什么需要加密?三、常见的加密方式3.1 对称加密3.2非对称加密四、

MySQL中读写分离方案对比分析与选型建议

《MySQL中读写分离方案对比分析与选型建议》MySQL读写分离是提升数据库可用性和性能的常见手段,本文将围绕现实生产环境中常见的几种读写分离模式进行系统对比,希望对大家有所帮助... 目录一、问题背景介绍二、多种解决方案对比2.1 原生mysql主从复制2.2 Proxy层中间件:ProxySQL2.3

python使用Akshare与Streamlit实现股票估值分析教程(图文代码)

《python使用Akshare与Streamlit实现股票估值分析教程(图文代码)》入职测试中的一道题,要求:从Akshare下载某一个股票近十年的财务报表包括,资产负债表,利润表,现金流量表,保存... 目录一、前言二、核心知识点梳理1、Akshare数据获取2、Pandas数据处理3、Matplotl

python panda库从基础到高级操作分析

《pythonpanda库从基础到高级操作分析》本文介绍了Pandas库的核心功能,包括处理结构化数据的Series和DataFrame数据结构,数据读取、清洗、分组聚合、合并、时间序列分析及大数据... 目录1. Pandas 概述2. 基本操作:数据读取与查看3. 索引操作:精准定位数据4. Group

MySQL中EXISTS与IN用法使用与对比分析

《MySQL中EXISTS与IN用法使用与对比分析》在MySQL中,EXISTS和IN都用于子查询中根据另一个查询的结果来过滤主查询的记录,本文将基于工作原理、效率和应用场景进行全面对比... 目录一、基本用法详解1. IN 运算符2. EXISTS 运算符二、EXISTS 与 IN 的选择策略三、性能对比

MySQL 内存使用率常用分析语句

《MySQL内存使用率常用分析语句》用户整理了MySQL内存占用过高的分析方法,涵盖操作系统层确认及数据库层bufferpool、内存模块差值、线程状态、performance_schema性能数据... 目录一、 OS层二、 DB层1. 全局情况2. 内存占js用详情最近连续遇到mysql内存占用过高导致

深度解析Nginx日志分析与499状态码问题解决

《深度解析Nginx日志分析与499状态码问题解决》在Web服务器运维和性能优化过程中,Nginx日志是排查问题的重要依据,本文将围绕Nginx日志分析、499状态码的成因、排查方法及解决方案展开讨论... 目录前言1. Nginx日志基础1.1 Nginx日志存放位置1.2 Nginx日志格式2. 499

Olingo分析和实践之EDM 辅助序列化器详解(最佳实践)

《Olingo分析和实践之EDM辅助序列化器详解(最佳实践)》EDM辅助序列化器是ApacheOlingoOData框架中无需完整EDM模型的智能序列化工具,通过运行时类型推断实现灵活数据转换,适用... 目录概念与定义什么是 EDM 辅助序列化器?核心概念设计目标核心特点1. EDM 信息可选2. 智能类