排序(5)——归并排序

六、归并排序

1.简介

        归并排序也是一种很经典的排序算法,采用分治的思想方法进行数据的处理。归并讲究的是先拆后合,也就是分治中的分而治之。在拿到一组数据后,程序会将整个数据进行不断拆分直至有序,因为单个元素必然有序,所以归并排序最后拆分形成的组每组只有一个元素。对于这些已经有序的组别,再进行治,就是合并两个有序序列。如此采取先分再合的策略使得一组数据最后成为有序。

2.思路与代码

(1)归并排序---递归

         首先需要理解归并排序的工作流程。归并排序首先对元素进行分割,在不断的分割后形成了一批只有一个元素的组,这些组因为只有一个元素,所以可以被认为是有序的。在将所有元素都分割成为有序序列后,便可以放心地进行合并了。合并就是将两个有序数组合并为一个有序数组,通过合并操作,我们可以将这些有序序列不断合并最后完成整个数组的排序。

        

        接下来梳理一下归并排序的流程和一些要点,对于一个数组a:

        ①我们仔细观察会发现,所谓分割就是将数据的处理范围进行缩小,从最初的整个数组,每一次简化一半的规模直到规模为一个元素,此时相当于完成了分割。然后就是对这些小规模的数据组进行合并再合并完成排序。分割部分实际就是简单的收缩范围,并未对数据做出什么处理;合并部分则是有一种逆着分割的感觉,并对数据进行了关键的排序。如此可以发现,归并排序实际就是将数组不断分为左右两部分,再分割结束后再进行逐层处理,这就是我们之前所说过的后序遍历的情况。所以我们在递归实现归并排序的过程中可以参考后序遍历的模板。

        ②合并时是要完成排序操作的,所以对两个有序数组进行合并生成一个有序数组也是我们需要解决的问题。这个问题并不困难,只需要借助辅助空间,比较两个数组元素小的进入辅助数组即可。这也意味着我们需要额外的一个临时数组来帮助我们进行数组的合并操作。

        ③完成代码时,我们因为既需要递归,又需要开辟数组,所以最佳解决方案就是将二者分离为两个函数,这样就可因避免递归导致反复开空间,难以管理。

//归并排序
void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int mid = (end + begin) / 2;

	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);

	int head1 = begin;
	int head2 = mid + 1;
	int cur = begin;
	while (head1 <= mid && head2 <= end)
	{
		if (a[head1] > a[head2])
		{
			tmp[cur++] = a[head2++];
		}
		else
		{
			tmp[cur++] = a[head1++];
		}
	}
	while (head1 <= mid)
	{
		tmp[cur++] = a[head1++];
	}
	while (head2 <= end)
	{
		tmp[cur++] = a[head2++];
	}
	for (int i = begin; i <= end; i++)
	{
		a[i] = tmp[i];
	}
}

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);
}

(2)归并排序---非递归

        在快速排序的递归改非递归时,我们指出递归变为非递归一般有两种途径:栈或者循环。像是快排一般的前序遍历使用栈是很顺手的,因为前序遍历先处理根结点,在根结点处理完后才在栈中存储接下来要处理的数据,这时根结点处理完成早已出栈,并无什么影响。但是归并排序属于后序遍历,相当于我们需要借助根结点得到接下来处理的数据,然后根结点不可以释放,因为最后仍需要处理根结点(合并)。相当于归并排序根结点需要二次使用,虽然我们可以使用两个栈,或者在入栈前先找到下一个区间再入栈等方法解决这个问题,但是嘛该用循环就要用,这里我们就用循环实现归并排序的非递归。

        因为归并排序的后序遍历注定了分割是“虚假的”,即我们可以主观认为数组中元素已经被分割好了。所以我们给出一个变量gap来标识合并过程下两个数组的规模。每一轮合并结束后gap就翻倍,这与递归下的逻辑相同。

        在具体的一轮排序中,需要很清晰两组数据的起始和终止位置。以变量i作为第一组数据的起始下标,因为一组数据规模是gap,所以第一组数据终止下标为i+gap-1。同理可以得到第二组数字的区间为[i+gap,i+2*gap-1]。当需要处理下一对时,给i加上2*gap即可。明确了这一点后,合并过程的排序逻辑就与递归无异了。

        最后需要注意的一点是不完全数组。当执行到一轮合并的最后一组数据时,因为我们的起始位置和终止位置都是根据i和gap计算所得的,所以需要判断并限制越界访问。当end1或begin2越界时,就说明不存在第二组数据,所以也就不需要进行合并操作了,直接终止循环即可;当只有end2越界时说明第二组数据和第一组数据规模不同,为了防止越界,只需要把第二组数据的终止下标end2人为改为整个数组最后一个元素下标就可以防止越界了。

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;//归并中第一个区间的范围
			int begin2 = i + gap, end2 = i + 2 * gap - 1;//归并中第二个区间的范围
			int head1 = begin1, head2 = begin2;
			int cur = begin1;
			if (end1 >= n || begin2 >= n)//数据只存在于第一个区间中,而第二个区间没有数据,这时无需归并(因为只有一组数据已然有序)
			{
				break;
			}
			if (end2 >= n)//第二个区间数据不满,此时需要对第二个区间进行调整,然后再归并
			{
				end2 = n - 1;//这种情况只会在最后一组出现,所以直接赋值end2为序列最后一个元素下标即可
			}
			while (head1 <= end1 && head2 <= end2)
			{
				if (a[head1] < a[head2])
				{
					tmp[cur++] = a[head1++];
				}
				else
				{
					tmp[cur++] = a[head2++];
				}
			}
			while (head1 <= end1)
			{
				tmp[cur++] = a[head1++];
			}
			while (head2 <= end2)
			{
				tmp[cur++] = a[head2++];
			}
			for (int i = begin1; i <= end2; i++)
			{
				a[i] = tmp[i];
			}
		}
		gap *= 2;
	}
	free(tmp);
}

3.复杂度与稳定性分析

(1)时间复杂度

        因为归并排序和快速排序都是类似于二叉树的分治过程,所以归并排序的时间复杂度和快速排序的计算方法一样,每一层都是一次数组的遍历,不同的是快排层数在log_2nn之间不确定,而归并排序层数很明显是log_2n层。

        因此归并排序时间复杂度为O(n*logn)

(2)空间复杂度

        归并排序使用了一个临时数组作为辅助与拷贝,再加上递归开销,其空间开销为n+log_2n,根据大O渐进表示法,其空间复杂度为O(n)

        我们所掌握的诸如快排、堆排序之类的排序算法一般称为内排序,因为他们都是在对内存中的数据进行排序。而归并排序虽然有O(n)的空间复杂度,但这也意味着归并排序可以应用于外排序,即对文件的数据排序中。在用归并排序进行外排序的时候,我们可以将一个新文件作为额外的空间使用,于是就可以在文件中排序了。

(3)稳定性

        归并排序是稳定的。

        归并排序对数据是整块整块处理的,可以说泾渭分明,相对位置维持的很好,所以是稳定的。

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

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

相关文章

【类和对象】4

日期类的拓展 c语言中的printf函数只能打印内置类型&#xff0c;为了弥补这一不足&#xff0c;c利用运算符重载可以打印自定义类型。 void operator<<(ostream&out);//声明在date.h中void Date::operator<<(ostream& out)//定义在date.cpp中 {out<<…

Flutter解析后台发来的jwt字段数据,了解jwt是否过期

前言 了解JWT是什么可以看这一篇博文 JWT&#xff08;JSON Web Token&#xff09;详解以及在go-zero中配置的方法-CSDN博客 流程 采用jwt_decoder库 添加至pubspec.yaml jwt_decoder: ^2.0.1 解析字段 查看是否过期 获取过期时间和token颁发的年龄

PythonStudio 控件使用常用方式(五)TListBox

PythonStudio是一个极强的开发Python的IDE工具&#xff0c;它使用的是Delphi的控件&#xff0c;常用的内容是与Delphi一致的。但是相关文档并一定完整。现在我试试能否逐步把它的控件常用用法写一点点&#xff0c;也作为PythonStudio的参考。 TListBox是列表框 常用属性 col…

【操作系统·考研】I/O管理概述

1.I/O设备 1.1 块设备 信息交换以数据块为单位&#xff0c;它属于有结构设备。 块设备传输速率较高&#xff0c;可寻址&#xff0c;且可对该设备随机地的读写。 栗子&#x1f330;&#xff1a;磁盘。 1.2 字符设备 信息交换以字符为单位&#xff0c;属于无结构类型。 字符…

LabVIEW叶片厚度远程监控

LabVIEW叶片厚度远程监控 随着网络技术的高速发展&#xff0c;远程监控广泛应用在各个领域。本文介绍了一种基于LabVIEW的植物叶片厚度远程监控系统&#xff0c;旨在实现对植物生长状况的精准监测和分析。 该系统利用LabVIEW软件开发工具&#xff0c;通过TCP网络协议实现数据…

关于反爬虫的的概述

目录 前言 一、验证码验证 二、IP限制 三、User-Agent限制 四、动态页面加载 总结 前言 反爬虫是一种防止网站被自动程序&#xff08;爬虫&#xff09;访问和抓取数据的技术手段。在网络爬虫的发展和使用过程中&#xff0c;有一部分爬虫是用于非法获取网站数据、侵犯隐私…

删除有序数组中的重复项 II[中等]

优质博文&#xff1a;IT-BLOG-CN 一、题目 给你一个有序数组nums&#xff0c;请你原地删除重复出现的元素&#xff0c;使得出现次数超过两次的元素只出现两次 &#xff0c;返回删除后数组的新长度。不要使用额外的数组空间&#xff0c;你必须在原地修改输入数组并在使用O(1)额…

Java基于SpringBoot+Vue的美容院管理系统,附源码,文档

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

20240202在Ubuntu20.04.6下使用whisper.cpp的显卡模式

20240202在Ubuntu20.04.6下使用whisper.cpp的显卡模式 2024/2/2 19:43 【结论&#xff1a;在Ubuntu20.04.6下&#xff0c;确认large模式识别7分钟中文视频&#xff0c;需要356447.78 ms&#xff0c;也就是356.5秒&#xff0c;需要大概5分钟&#xff01;效率太差&#xff01;】 …

electron项目在内网环境的linux环境下进行打包

Linux需要的文件: electron-v13.0.0-linux-x64.zip appimage-12.0.1.7z snap-template-electron-4.0-1-amd64.tar.7z 下载慢或者下载失败的情况可以手动下载以上electron文件复制到指定文件夹下&#xff1a; 1.electron-v13.0.0-linux-x64.zip 复制到~/.cache/electron/目录下…

web前端--------渐变和过渡

线性渐变&#xff0c;是指颜色沿一条直线进行渐变&#xff0c;例如从上到下、从左到右。 当然&#xff0c;CSS中也支持使用角度来设置渐变的方向&#xff0c;角度单位为deg。 0deg&#xff0c;为12点钟方向&#xff0c;表示从下到上渐变。 90deg&#xff0c;为3点钟方向&…

海外社媒营销平台及运营规则,如何降低封号率?

社交媒体已经成为人们生活和日常习惯不可或缺的一部分&#xff0c;在跨境电商出海过程中&#xff0c;海外社媒营销平台可以起到非凡的助力&#xff1b;而平台的选择以及平台的运营技巧、规则都各有不同。很多海外社媒工作者经常会被封号&#xff0c;这也是难度之一&#xff0c;…

吸猫毛空气净化器哪个好?推荐除猫毛效果好的宠物空气净化器品牌

如今&#xff0c;越来越多的家庭选择养宠物&#xff0c;使家庭变得更加温馨。然而&#xff0c;养宠物可能会带来异味和空气中的毛发增多&#xff0c;这可能会成为一大困扰&#xff0c;并对健康造成问题。 为了不让家里充斥着异味&#xff0c;特别是来自宠物便便的味道&#xf…

无人零售模式下,“IoT+鸿蒙”实现零代码搭建自动售货机监控大屏的可能性摸索

前言 新零售模式下&#xff0c;对loT的探索与应用还在继续。 而数字时代&#xff0c;数字化转型在零售行业中蔓延&#xff0c;而对于新的消费方式的探索&#xff0c;也在如火如荼的进行中。于是&#xff0c;一种新零售的形式——无人零售逐渐形成概念。 如果说&#xff0c;人…

微信小程序新手入门教程二:认识JSON配置文件

在上一篇我们介绍了微信小程序的注册和基本使用方式&#xff0c;并且写出了一个简单的页面&#xff0c;但是依然没有解释目录中的各种.json文件是做什么的。这篇我们就来认识一下各种JSON配置文件及其配置项。 一 认识JSON 首先先来认识一下JSON是什么。 JSON 指的是 JavaScri…

25.泛型

泛型 1.泛型1.1 概述1.2 代码示例 2. 泛型类2.1 概述2.2 代码示例 3. 泛型方法3.1 概述3.2 代码示例 4. 泛型接口4.1 概述4.2 代码示例 5. 泛型特点5.1 概述5.2 代码示例 6. 泛型通配符6.1 概述6.2 代码示例 7. 综合案例8. 注意事项 1.泛型 泛型是Java编程语言的一项重要功能&…

故障诊断 | 一文解决,CNN-LSTM卷积神经网络-长短期记忆神经网络组合模型的故障诊断(Matlab)

效果一览 文章概述 故障诊断 | 一文解决,CNN-LSTM卷积神经网络-长短期记忆神经网络组合模型的故障诊断(Matlab) 模型描述 CNN-LSTM模型是一种结合了卷积神经网络(Convolutional Neural Network)和长短期记忆神经网络(Long Short-Term Memory)的组合模型,常用于数据故障…

FPGA解码MIPI视频:Xilinx Artix7-35T低端FPGA,基于MIPI CSI-2 RX Subsystem架构实现,提供工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐我这里已有的 MIPI 编解码方案本方案在Xilinx Artix7-100T上解码MIPI视频的应用本方案在Xilinx Kintex7上解码MIPI视频的应用本方案在Xilinx Zynq7000上解码MIPI视频的应用本方案在Xilinx Zynq UltraScale上解码MIPI视频的应用纯VHDL代码解…

vite和vue-cli实现原理和优化及区别

Vite&#xff1a; 1. 实现原理&#xff1a; Vite 是一个基于 ESModule 的构建工具。它利用原生 ESModule 的特性&#xff0c;将每个文件作为一个模块&#xff0c;通过浏览器去解析和执行&#xff0c;而不需要提前将文件打包成一个单独的 bundle。Vite 利用浏览器的原生 ESMod…

适用于汽车 4D 成像雷达的双器件毫米波级联参考设计(TI文档)

说明 该汽车雷达参考设计是一个 76GHz 至 81GHz 的级联雷达传感器模块。这包括由 AWR2243 器件和AM2732R 雷达处理器构成的双器件级联阵列。在这一级联雷达配置中&#xff0c;一个主器件向主器件和辅助器件分配20GHz 的本机振荡器 (LO) 信号&#xff0c;使这两个器件作为单个射…
最新文章