【数据结构与算法】(15):归并排序的递归和非递归方式

🤡博客主页:Code_文晓

🥰本文专栏:数据结构与算法

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多数据结构与算法点击专栏链接查看💛💜✨✨ 


        归并排序,英文名称是Merging Sort。归并排序里的“归并”,也叫合并,其实就是把两个或者多个已经有序的序列合并成一个。简而言之,归并排序采用的是一种分而治之的思想,将待排序的序列不断二分,直到每个子序列只有一个元素,然后将相邻的子序列两两归并,最终得到一个有序的序列。

下面,我们就一起看一看归并排序都涉及到哪些基本概念。

1. 概念和实例

        图1是两个有序序列(数组),现在我们希望利用归并排序把这两个数组合并成一个元素按从小到大排序的数组。  

        既然要合并,那么首先必然要分配一个足以容纳两个有序数组中所有元素的内存空间,也就是一个新数组,然后进行下面的操作。

  1. 针对每个数组都设置一个指向当前位置的指针(pi、pj、pk),初始状态时,这三根指针都指向数组的起始位置,如图2中的子图1。

  2. 每一次都会对比pi和pj位置所指向的元素值到底哪个更小,把更小的元素放入到pk所指向的位置处。因为1<2,所以把1放入到pk指针所指向的位置处,同时,把pk指针后移一个位置。当然,因为元素1已经被放入到了新数组中,自然pi指针也要后移一个位置,结果如图2中的子图2。

  3. 重复上面第二个步骤,有那么一个时刻,有序序列1中的pi指针已经指向了最后一个元素后面的位置,这意味着有序序列1中的所有元素都被放入到了新数组中,只剩下有序序列2中还有没排好序的元素了,如图2中的子图3所示。

  4. 此时,就不再需要元素大小的对比,直接把有序序列2中剩余的2个元素放入到新数组中,最终归并排序结果如图2中的子图4。

        图2其实是一个“2路”归并排序。因为图中是把两个有序序列合并成一个有序序列。在2路归并排序中,每次需要从两个元素中选出一个更小的元素,这只需要对比pi和pj所指向的元素就可以了。或者你可以理解为,只需要对比一次关键词。

        既然有2路归并排序,那么自然可以有多路归并排序,比如3路归并排序、4路归并排序等等。这里注意,多路归并排序一般用于外部排序。图3展示的就是4路归并排序的初始状态。

        针对图3,如果进行4路归并排序(对4个有序序列进行归并排序),那么每选出一个最小元素放入新数组中需要进行3次关键字比较——比如第1次需要比较元素1和2看谁比较小,选择出较小的元素继续与元素42相比看谁比较小,再选出较小的元素与元素28相比。推而广之,对于m路归并排序,每选出一个元素需要进行m-1次关键字的对比。内部排序中通常使用的是2路归并排序。

那么,回头就要说一说如何对一个具有n个记录的初始序列进行排序了。

  • 首先,把这个初始序列看成是n个有序的子序列(n路),每个子序列的长度为1。

  • 其次,两两合并(“2路”归并排序),得到\left \lceil \frac{n}{2} \right \rceil个长度为2或1的有序子序列,然后再两两合并……直至得到一个长度为n的有序序列为止。

        如图4所示,将16、1、45、23、99、2、18共7个数字进行归并排序:

  • 第一趟归并排序,将相邻部分两两归并,即16和1归并,45和23归并,99和2归并,18由于只剩下自己了所以不做归并操作。经过这趟归并,得到了4个有序的子序列。

  • 第二趟归并排序,对第一趟归并排序的结果再次进行两两归并,即[1,16]和[23,45]归并,[2,99]和[18]归并。经过这趟归并,得到了两个有序的子序列。

  • 第三趟归并排序,对第二趟归并排序的结果再进行两两归并,最终得到了一个排好序的序列。

2. 递归实现代码 

void _MergeSort(int* myarray, int begin, int end, int* tmp)
{
	if (begin == end)  // 如果起始位置和结束位置相同,表示只有一个元素,已经有序,直接返回
	{
		return;
	}

	int mid = (begin + end) / 2;  // 计算中间位置

	_MergeSort(myarray, begin, mid, tmp);  // 对左半部分进行递归排序
	_MergeSort(myarray, mid + 1, end, tmp);  // 对右半部分进行递归排序

	int begin1 = begin, end1 = mid;  // 第一组的起始索引和结束索引
	int begin2 = mid + 1, end2 = end;  // 第二组的起始索引和结束索引
	int i = begin;  // 临时数组的索引

	while (begin1 <= end1 && begin2 <= end2)  // 当两组都还有元素时进行比较
	{
		if (myarray[begin1] <= myarray[begin2])  // 如果第一组的元素小于等于第二组的元素
		{
			tmp[i++] = myarray[begin1++];  // 将第一组的元素放入临时数组中,并增加相应的索引
		}
		else
		{
			tmp[i++] = myarray[begin2++];  // 将第二组的元素放入临时数组中,并增加相应的索引
		}
	}

	while (begin1 <= end1)  // 如果第一组还有剩余元素,则将剩余元素依次放入临时数组中
	{
		tmp[i++] = myarray[begin1++];
	}

	while (begin2 <= end2)  // 如果第二组还有剩余元素,则将剩余元素依次放入临时数组中
	{
		tmp[i++] = myarray[begin2++];
	}

	/*
	为什么要使用 myarray+begin 和 tmp+begin 作为参数呢?
	这是因为在归并排序的过程中,我们只对数组的一个区间进行排序,而不是整个数组。
	起始位置 begin 表示排序区间的起始位置,所以我们需要将起始位置对应的内存块复制回原数组的相应位置。
	*/
	memcpy(myarray + begin, tmp + begin, sizeof(int) * (end - begin + 1));  // 将临时数组中归并后的结果拷贝回原数组中,从起始位置开始

}

void MergeSort(int* myarray, int length)
{
	int* tmp = (int*)malloc(sizeof(int) * length);  // 创建临时数组用于存储归并结果
	_MergeSort(myarray, 0, length - 1, tmp);  // 进行递归排序
	free(tmp);  // 释放临时数组的内存空间
}

3. 归并排序复杂度分析 

        图4所示的2路归并排序过程看起来是一棵倒着的二叉树,因此又被称为“归并树”。如果要对n个元素进行排序,把二叉树的高度看成是h,则归并排序的趟数是h-1趟。而二叉树的第h层最多有2^{h}-1个节点。因为所有待排序列的节点都会出现在第h层,所以这也是待排序列的节点总数,即h-1=\left \lceil log^{n}_{2} \right \rceil。也就是说,n个元素进行2路归并排序的趟数是\left \lceil log^{n}_{2} \right \rceil。每一趟归并操作的时间复杂度是多少呢?观察一下图2、图4,不难发现是需要将所有n个元素都扫描一次并进行相应对比,所以每一趟归并操作的时间复杂度是O(n),最后得出结论——归并排序算法的时间复杂度是O(nlog^{n}_{2})

        此外,从上述代码可以看到,归并排序需要额外的辅助空间,辅助空间的大小和原来保存数据的数组大小相同,所以这部分的空间复杂度是O(n)。算法中还用到了递归调用,但递归调用的深度不会超过log^{n}_{2}。所以归并排序算法的空间复杂度是O(n+log^{n}_{2})= O(max(n,log_{2}^{n}))=O(n)

归并排序算法的稳定性受编写代码的影响,上述的代码实现方式得到的归并排序算法是 稳定 的。


4. 非递归实现代码 

        在归并排序的非递归版本中,我们使用了一个临时数组来存储归并的结果。初始时,我们将步长设置为1,然后进行多轮的迭代,每轮迭代都将步长翻倍。在每轮迭代中,我们以两倍步长的间隔遍历数组,将数组分为多个小组,每个小组的长度为步长。对于每个小组,我们会比较并归并相邻的两个子序列。

        具体而言,我们使用四个指针来标记两个子序列的起始和结束位置。在每次归并操作中,我们将两个子序列的元素进行比较,并按照从小到大的顺序依次放入临时数组中。如果其中一个子序列的元素已经全部放入临时数组中,而另一个子序列还有剩余元素,则直接将剩余元素放入临时数组中。最后,我们将临时数组中的归并结果拷贝回原始数组的相应位置。

        随着迭代的进行,步长逐渐增加,小组的长度逐渐变大,直到整个数组被归并排序为止。最后,我们释放临时数组的内存空间。

// 归并排序非递归版
void MergeSortNonR(int* myarray, int length)
{
    int* tmp = (int*)malloc(sizeof(int) * length);  // 创建临时数组用于存储归并结果
 
    int gap = 1;  // 初始步长为1
    while (gap < length)  // 当步长小于数组长度时
    {
        int j = 0;  // 临时数组索引
        for (int i = 0; i < length; i += 2 * gap)  // 每次以两倍步长遍历数组
        {
            int begin1 = i, end1 = i + gap - 1;  // 第一组的起始索引和结束索引
            int begin2 = i + gap, end2 = i + 2 * gap - 1;  // 第二组的起始索引和结束索引
 
            if (end1 >= length || begin2 >= length)  // 如果某一组的索引超出数组范围,则跳出循环
            {
                break;
            }
 
            if (end2 >= length)  // 如果第二组的结束索引超出数组范围,则修正为数组最后一个元素的索引
            {
                end2 = length - 1;
            }
 
            while (begin1 <= end1 && begin2 <= end2)  // 当两组都还有元素时进行比较
            {
                if (myarray[begin1] < myarray[begin2])  // 如果第一组的元素小于第二组的元素
                {
                    tmp[j++] = myarray[begin1++];  // 将第一组的元素放入临时数组中,并增加相应的索引
                }
                else
                {
                    tmp[j++] = myarray[begin2++];  // 将第二组的元素放入临时数组中,并增加相应的索引
                }
            }
 
            while (begin1 <= end1)  // 如果第一组还有剩余元素,则将剩余元素依次放入临时数组中
            {
                tmp[j++] = myarray[begin1++];
            }
 
            while (begin2 <= end2)  // 如果第二组还有剩余元素,则将剩余元素依次放入临时数组中
            {
                tmp[j++] = myarray[begin2++];
            }
 
            memcpy(myarray + i, tmp + i, sizeof(int) * (end2 - i + 1));  // 将临时数组中归并后的结果拷贝回原数组中
        }
    
        gap *= 2;  // 增加步长为原来的两倍
    }
 
    free(tmp);  // 释放临时数组的内存空间
}

归并排序的特性总结:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

二、C#选择排序算法

简介 选择排序算法的基本思想是每一次从待排序的数据元素中选出最小&#xff08;或最大&#xff09;的一个元素&#xff0c;存放在序列的起始位置&#xff0c;然后&#xff0c;再从剩余未排序元素中继续寻找最小&#xff08;大&#xff09;元素&#xff0c;然后放到已排序序列…

【ArcGISProSDK】获取扩展模块许可到期时间

结果 以下是获取的3D分析模块的许可到期时间 代码 var licenseExpirationDate ArcGIS.Core.Licensing.LicenseInformation.GetExpirationDate(LicenseCodes.Analyst3D); 扩展模块 MemberDescriptionAnalyst3D3D AnalystAviationAirportsAviation and AirportsBusinessAnal…

绿色再生·安卓4G智能远程操作巡视机器人小车

一、前言 1.1 项目介绍 【1】项目功能介绍 随着物联网技术与移动通信技术的快速发展&#xff0c;远程遥控设备在日常生活及工业应用中的普及度日益提高。无论是家用扫地机器人实现自主导航清扫&#xff0c;还是目前抖音平台上展示的实景互动小车等创新应用&#xff0c;都体现…

ICBatlas数据库-转录组免疫检查点阻断疗法数据

ICBatlas: A Comprehensive Resource for Depicting Immune Checkpoint Blockade Therapy Characteristics from Transcriptome Profiles 介绍&#xff1a;在线ICBatlas (hust.edu.cn) 检查点阻断 &#xff08;ICB&#xff09; 疗法为多种癌症类型提供了显着的临床益处。目前…

6语言交易所/多语言交易所php源码/微盘PHP源码

6语言交易所PHP源码&#xff0c;简单测试了一下&#xff0c;功能基本都是正常的。 由于是在本地测试的运行环境的问题&#xff0c;K线接口有点问题&#xff0c;应该在正式环境下是OK的。 源码下载地址&#xff1a;6语言交易所/多语言交易所php源码/微盘PHP源码.zip 程序截图…

【安装教程】安装cudnn

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 安装cudnn 前言一、下载和自己cuda匹配的cudnn二、安装cudnn 前言 首先我是已经安装了cuda&#xff0c;但是因为没有安装cudnn在跑程序的时候出现了一些问题&#xff0c;因此…

Axure 中继器的Repeater属性的使用

dataCount 中继器当中存在多少条数据&#xff0c;总数。 visibleltemCount 中继器列表中可见项数量&#xff0c;也就是当前页面显示的数量。 pageCount 获取中继器分页的总数量&#xff0c;即能够获取分页后共有多少页。 pageIndex 获取中继器当前显示的页码

【数字孪生】Nginx发布数字孪生三维建模模型服务及调用方法

【数字孪生】Nginx发布数字孪生三维建模模型服务及调用方法 一、需求二、实施步骤2.1 准备模型文件2.1.1 3D tiles模型2.1.2 3D Tiles标准文件格式 2.2 配置nginx server块2.2.1 Nginx能干啥 2.3 访问 三、实现效果 一、需求 利用三维渲染引擎Cesium加载3D tiles模型。 二、实…

上海微电子企业ERP系统介绍及现状

在当今信息化、数字化的时代&#xff0c;企业资源规划(ERP)系统已成为企业管理的核心工具。上海微电子企业&#xff0c;作为国内微电子行业的重要力量&#xff0c;其ERP系统的应用与发展更是备受关注。 ERP系统是一种集信息技术与管理思想于一体的企业管理系统。它通过对企业内…

无人咖啡机品质之选,D 咖助力差异化竞争

在当今竞争激烈的商业环境中&#xff0c;如何脱颖而出成为众多企业关注的焦点。而无人咖啡机的出现&#xff0c;为商家提供了一个全新的思路。D 咖无人咖啡机&#xff0c;以其卓越的品质和独特的功能&#xff0c;成为了商家们实现差异化竞争的得力助手。 1. 卓越品质&#xff1…

uniapp——第4篇:分析一下全局文件、配置

前提&#xff0c;建议先学会前端几大基础&#xff1a;HTML、CSS、JS、Ajax&#xff0c;还有一定要会Vue!&#xff08;Vue2\Vue3&#xff09;都要会&#xff01;&#xff01;&#xff01;不然不好懂 一、uniapp项目创建新包放乱杂文件 我们的项目结构里有一个包叫static&#x…

Python环境下基于1D-CNN、2D-CNN和LSTM的一维信号分类

以简单的西储大学轴承数据集为例&#xff0c;随便你下载几个信号玩耍吧&#xff0c;我选了10个信号&#xff0c;分别求为正常状态&#xff0c;内圈&#xff08;轻、中和重度损伤&#xff09;&#xff0c;外圈&#xff08;轻、中和重度损伤&#xff09;&#xff0c;滚动体&#…

分类预测 | Matlab实现BiTCN双向时间卷积神经网络数据分类预测/故障识别

分类预测 | Matlab实现BiTCN双向时间卷积神经网络数据分类预测/故障识别 目录 分类预测 | Matlab实现BiTCN双向时间卷积神经网络数据分类预测/故障识别分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现BiTCN双向时间卷积神经网络数据分类预测/故障识别。 2.自…

4个环节5大方面,助您打造“标准化仓库”

仓库管理&#xff0c;在保障企业物流运作效率、降低运营成本、提高客户服务质量等方面发挥着不可替代的作用。标准化、规范化管理作为仓库管理中的重要手段&#xff0c;不仅能够提高管理效率&#xff0c;还能够有效地降低管理风险&#xff0c;使仓库运作更加安全、稳定、高效。…

弧光保护装置助力煤矿高压开关柜的可靠供电

在煤矿高压开关柜运行中&#xff0c;由于受到多种因素的干扰&#xff0c;中低压母线发生故障的概率较高&#xff0c;在中低压母线装设中又没有设置专门的保护&#xff0c;所以开关柜电弧光短路等问题时有发生&#xff0c;对变压器等设备造成一定的损害。鉴于此&#xff0c;对电…

3.16美团笔试复盘

3.16美团复盘 第一题 题目分析 这一题比较简单&#xff0c;直接模拟即可&#xff0c;sum(a)-t1-t2; import java.util.*; class Main{public static void main(String[] args){Scanner scnew Scanner(System.in);int nsc.nextInt();long sum0;while(n-->0){sumsc.nextInt()…

宜搭生产情况调试技巧

在宜搭生产环境中&#xff0c;经常会碰到一些人对于某些操作有报错&#xff0c;即便是有错误日志&#xff0c;但是错误日志没发获取详细的错误栈和错误信息,因此对于复刻某些操作是有必要的&#xff0c;这里我给出一个还蛮好用的方法。 首先找到错误操作的数据来源&#xff0c…

盛元广通全新智能实验室管理系统3.0强势上线

目前&#xff0c;盛元广通全新上线智能实验室管理系统3.0&#xff0c; 在原有的产品基础上迭代更新升级&#xff0c;此次升级以“新势能、智能化、低代码化”为产品功能赋能&#xff0c;从界面的整体布局、数据的维度、和兼容、安全性方面都实现了质的提升&#xff0c;系统在易…

代码随想录算法训练营第14天 part01 | 二叉树理论基础篇

代码随想录 二叉树理论基础篇 二叉树的种类 二叉树有两种主要的形式&#xff1a;满二叉树和完全二叉树 满二叉树&#xff1a;如果一棵二叉树只有度为0的结点和度为2的结点&#xff0c;并且度为0的结点在同一层上&#xff0c;则这棵二叉树为满二叉树。 这棵二叉树为满二叉树…

2023年迟到的年终总结

前言 一年又一年&#xff0c;过的好快啊。本以为上一年是黑云压城城欲摧&#xff0c;2023年可以甲光向日金鳞开&#xff0c;但是没想到23年更是黑暗啊&#xff0c;人生嘛总是十之八九不如意&#xff0c;一年的经历可以大概的概括为&#xff0c;业务转型的Q1&#xff0c;压力倍增…