归并排序深度剖析

news2024/11/26 7:35:48

目录

 

一、什么是归并排序?

二、归并排序的实现

三、归并排序非递归


一、什么是归并排序?

        归并排序是建立在归并操作上的一种有效,稳定 的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。                  ————百度百科

        实际上 归并排序MergeSort)是建立在归并操作上的一种排序算法,利用 分治 的思想来,将要排序的数据进行 逐层分组,一组分两组,两组分四组...直到分到只有一个元素,这个时候在和并元素的同时对元素进行排序,按照之前的元素分组在一一排序合并到新数组最后拷贝回原数组的操作。操作如下图:

bdae3863ecb74353926bdf75446505e0.png

归并排序动图演示:

2021032413100384.gif#pic_center


二、归并排序的实现

         归并排序虽然我们展示的是线性结构,但是我们经常以树形结构来看先将数组进行分组,进行对半分(并不要求一定要对半分),假设我们拿上图的八个数据,先将8个数据进行44分,如果有区间还是属于无序状态就进行22分,直到分到只剩一个元素

        之后将元素排序再按照22归拷贝到新数组tmp,22归完进行44归,这样就有序了。其实这种排序方式是树形结构的一种便利方式——后序遍历 方式(如果对二叉树不了解可以看看我的这篇文章:二叉树详解)。

        首先我们可以在进行排序之前先开辟一个tmp数组来记录归并的值:

void MergeSort(int* a, int len)//传入要排序的数组,以及数组的长度
{
	int* tmp = new int[len];//开辟与数组长度相同的归并用的数组
	_MergeSort(a, 0, len - 1, tmp);//真正进行归并排序,传入要排序的左区间与右区间和数组长度与两个数组
}

         接下来进入归并排序,首先是先将数据递到最深,前面说了,归并排序也就相当于二叉树的后序遍历(先向左右子树递归,递归到最深处):

void _MergeSort(int* a, int left, int right, int *tmp)
{
	if (left >= right)//注意,当左区间要等于右区间表示此时已经递归到只剩一个元素,大于右区间显然不能在继续递归下去了
		return;

	int mid = (left + right) / 2;  //首先选取排序的区间,将区间分为左右区间,在对左右区间分别递归,在对半分在递归...
	_MergeSort(a, left, mid, tmp);//后序遍历顺序:left——right——root,这里先遍历左子树
	_MergeSort(a, mid + 1, right, tmp);//在遍历右子树

    //..
}

              这里我画出递归展开图帮助大家理解一下:f55e5585e5de4294aad18f7787fa8866.png

        接下来就是向上归的过程,在向上归的过程中,首先要控制好区间,我们前面将数组分成两份,一份从begin到mid,一份从mid+1到end。那么该如何确定哪个元素大哪个元素小呢?其实很简单:

        1、创建五个变量,分别记录两个区间的起始和结束(begin1, end1   begin2, end2),最后一个记录tmp数组的下标(index)。

        2、需要一个while循环,首先循环的条件肯定是两个区间的起始位置都要小于等于终止位置(可能存在其中一个区间没有进入到tmp数组里面,这是不确定的)。

        3、,在循环里比较两个区间的起始位置,哪个值较小就将值赋给tmp数组,tmp数组下标index自增,此元素下标也自增,否则另一个区间的元素进行赋值给tmp,下标同样都自增。

        4、最后,可能会存在左区间或者右区间的值是没有进入到tmp数组的,所以我们直接在来两个while循环对两个区间分别赋值给tmp,保证最后两个区间都进入到tmp数组。

        5、这些完成之后,将这两个区间的值拷贝回原数组,这里我们使用C语言中的memcpy函数进行拷贝,在拷贝回原数组时要拷对位置,从左区间第一个元素开始拷,tmp数组也要对应,拷贝字节大小为右区间减去左区间加一乘上整形字节数。

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

	int mid = (left + right) / 2;
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	int begin1 = left, begin2 = mid + 1;//两个区间的左区间
	int end1 = mid, end2 = right;//两个区间的右区间
	int index = left;//tmp数组的下标,记录tmp中已存在元素个数

	while (begin1 <= end1 && begin2 <= end2)//两个区间都不越界就继续比较赋值
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}

	while (begin1 <= end1)//第一个区间元素没入完就直接将剩下元素赋值到tmp
	{
		tmp[index++] = a[begin1++];
	}
	while (begin2 <= end2)//右区间同样如此
	{
		tmp[index++] = a[begin2++];
	}

	memcpy(a + left, tmp + left, (right - left + 1) * sizeof(int));//最后拷贝回原数组
}

        这样,归并排序就完成了,下面是完整的代码以及测试结果: 

#include<iostream>

using std::cout;
using std::cin;
using std::endl;

void _MergeSort(int* a, int left, int right, int *tmp)
{
	if (left >= right)
		return;

	int mid = (left + right) / 2;
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	int begin1 = left, begin2 = mid + 1;
	int end1 = mid, end2 = right;
	int index = left;

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}

	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}

	memcpy(a + left, tmp + left, (right - left + 1) * sizeof(int));
}

void MergeSort(int* a, int len)
{
	int* tmp = new int[len];
	_MergeSort(a, 0, len - 1, tmp);
}

void output(int* a, int len)
{
	int i = 0;
	for (i = 0; i < len; i++)
	{
		cout << a[i] << "  ";
	}
	cout << endl;
	return;
}

void Test()
{
	int a[] = { 6, 1, 4, 8, 2, 7, 3, 5 };
	int len = sizeof(a) / sizeof(int);

	MergeSort(a, len);
	output(a, len);

	return;
}

int main()
{
	Test();
	return 0;
}

e673eecc6af740678094f8148d76f3ec.png

总的来说,归并排序的递归写法还是很简单的。 


三、归并排序非递归

        归并排序说到底用的还是递归,用递归很容易就会造成 栈溢出,为了防止这种情况,我们有必要掌握归并排序的非递归编写方式。

        值得注意的是,这里我们如果用栈或者队列来模拟实现归并排序的非递归其实是很困难的,我们这里用一种别的方法:

        首先,我们先来对归并的过程详细的学习一下:

88060f5b23f74fc5847257f8bd09a096.png

        在归并的过程中首先进行11归,在进行22归,44归...直到把全部数据归并完毕,那么,我们该如何实现这种非递归模式呢?其实我让大家看归并的过程是有用的,我们可以像希尔排序那样设置gap间隔来分组(这里是分区间),比如gap == 1就代表11归,gap == 2就是22归,同时gap也是元素的个数

        1、在最外层用while循环控制gap的值。

        2、在循环内,用for循环来对每个归并的过程进行gap gap归,在for循环内每次循环跳2倍的gap,这样正好跳过这个已排序的区间,跳向下一个区间。

        3、for循环内的内容就和递归一样了,while循环对分成的两个区间进行排序,最后拷贝回原数组,值得注意的是,这里的memcpy拷贝位置是要+i的(对应区间位置),大小是右区间的end减去左区间的begin在乘上sizeof(int)。

#include<iostream>

using std::cout;
using std::cin;
using std::endl;

void MergeSortNonR(int *a, int len)
{
	int *tmp = new int[len];//tmp数组
	
	int gap = 1;
	while(gap < len)//while循环控制gap gap 归
	{
		int i = 0;
		for(i = 0 ; i < len ; i += 2 * gap)//每次循环跳2倍gap就会跳过gapgap归的一个区间
		{
			int begin1 = i, end1 = i + gap - 1;//这里是下标,所以end最后要-1
			int begin2 = i + gap, end2 = i + 2 * gap - 1;//2倍的gap相当于跳两个区间

			int index = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
	
			memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));//拷贝回原数组
		}
		gap *= 2;
	}
}

void output(int* a, int len)
{
	int i = 0;
	for (i = 0; i < len; i++)
	{
		cout << a[i] << "  ";
	}
	cout << endl;
	return;
}

void Test()
{
	int a[] = { 6, 1, 4, 8, 2, 7, 3, 5 };
	int len = sizeof(a) / sizeof(int);

	MergeSortNonR(a, len);
	output(a, len);

	return;
}

int main()
{
	Test();
	return 0;
}

b16782b1e25645379981b33a35a55777.png

        可以看到,这样写似乎并没有什么问题,依旧能够排序,但是如果我的数据量为9个,10个,11个呢?这种写法能保证一定能完成排序吗,就按照gap gap归,while循环gap每次都乘2,当gap == 16的时候,这个时候在进行88分?这样一定是会越界访问的。

        我们不妨做一下测试:

9f71d314c7cb4907a15b64ed6e4491e4.png8f0581b05ba24fafb23b8ba90ee9559e.png

        我们在这里加上printf函数以便于更加直观的测试哪里出了问题,首先是原始八个数据:

bffb06c5e237484b9090d8be0568bc9c.png

        可以发现,8个数据是没任何问题的,区间是在0-7范围的,不会产生越界问题,但是我们将数据增加到9个呢?

14210f19eb4d4f67bf603140885ab3b2.png

37946ef59fe84d55992983842fdf2c0b.png

        我们就会发现这里程序直接挂了,我们前面也说了,这一定是越界问题,那么究竟是哪里越界了呢?

310596fbe49640d3abfa2170ea2d57db.png

7cc5473589f44ed3af4076d641850a6c.png

        我们增加了一个元素,所以数组的下标就为0-8九个元素,那么超过8的就都为越界,这里你可以先不往下看,思考思考,这里的越界分为几种情况?谁有可能要越界,我们该对谁管理? 

        经过上面的越界信息我们可以得到以下三种情况:

   1、[begin1, end1] [begin2, end2]中,begin2没越界,end2越界。

   2、[begin1, end1] [begin2, end2]中,begin2与end2都越界。

   3、[begin1, end1] [begin2, end2]中,end1与begin2,end2都越界。

        无论怎么越界,只要是发生了越界,end2就必定在其中,那么我们在归并的时候比如22归的时候,一定要两组数据都拥有两个数吗?不一定吧,我在递归里面也说了,分组没有那么严格,就算两组的元素个数不一样也是能归并的,甚至就算只有一组也是可以归并的。833f2f6feeee4d4696076544614e6790.png

         那么有什么好的办法来防止越界的发生呢?其实这里只需要加上两条if语句就行了:424a9560ac2e432d9bf9e407bac579d0.png

        我们如何理解这几行代码呢?

        1、当我们的end1越界了其实就可以直接break了,因为end1越界那么后边一定越界,end1也越界了,那么整个数组就剩下begin1一个元素所以就不用排序直接break,同样当begin2越界的话,右区间全部越界,左区间是已排好序的区间,那么直接break了对左区间也没有影响。

        2、随后在检测end2 如果end2越界了,该怎么办?其实很简单,因为end2已经是最后一个元素了,如果他越界了就表示右数组没有end2这个数据,那么我们就可以直接修正下标,使下标指向数组最后一个元素就不会发生越界了。

这个时候在运行就不会有任何越界的问题发生了:1970bc2266d440d0b14283ee6eb648ca.png

         其实在第一个if这里还有优化的方案:a7631dce39a7497e89ee45de08365f4a.png

        其实这里理解起来也很简单,begin2越界的时候第二组就全越界了,直接break掉,可能你会说:哦,确实有道理,但是吧,如果end1越界了的情况没有说明啊?

        其实啊,我们在对这两组数进行归并的时候,这两组数每组里面已经是有序的了,那么我直接不管end1越没越界,如果第二组全都越界了,我也就break了,如果此时我end1越界了,那我还有左半区间是有序的,不用归,如果end1没越界,我第一组本来就是有序的,我也不用归啊,这在一定程度上还减少了消耗。

归并排序非递归完整代码如下:

#include<iostream>

using std::cout;
using std::cin;
using std::endl;

void MergeSortNonR(int *a, int len)
{
	int *tmp = new int[len];
	
	int gap = 1;
	while(gap < len)
	{
		int i = 0;
		for(i = 0 ; i < len ; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			
			if(begin2 >= len)
			{
				break;
			}
			
			if(end2 >= len)
			{
				end2 = len - 1;
			}
			
			int index = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
	
			memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));
		}
		gap *= 2;
	}
}

void output(int* a, int len)
{
	int i = 0;
	for (i = 0; i < len; i++)
	{
		cout << a[i] << "  ";
	}
	cout << endl;
	return;
}

void Test()
{
	int a[] = { 6, 1, 4, 8, 2, 7, 3, 5, 11, 9, 10 };
	int len = sizeof(a) / sizeof(int);

	MergeSortNonR(a, len);
	output(a, len);

	return;
}

int main()
{
	Test();
	return 0;
}

测试结果:6804bb0ff8ea4956b3e2375dce5b66cd.png


b8e12fdfc3a8401182f022fc9021bc6e.jpeg如果各位看官老爷还满意的话,不如留下小小的三连支持一下博主吧~~

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

stm32 模拟spi

目录 简介 spi物理层 连接方式 框图 协议层&#xff1a; 数据处理 传输模式 模式0 起始和停止信号 发送和接收数据 模式1 模式2 模式3 总结 简介 spi物理层 SPI&#xff08; Serial Peripheral Interface&#xff0c; 串行外设接口&#xff09;是一种全双工同步…

Vlice DM蓝牙5.2双模热插拔PCB

键盘使用说明索引&#xff08;均为出厂默认值&#xff09; 软件支持&#xff08;驱动的详细使用帮助&#xff09;一些常见问题解答&#xff08;FAQ&#xff09;首次使用步骤蓝牙配对规则&#xff08;重要&#xff09;蓝牙和USB切换键盘默认层默认触发层0的FN键配置的功能默认功…

【51单片机】LED与独立按键(学习笔记)

一、点亮一个LED 1、LED介绍 LED&#xff1a;发光二极管 补&#xff1a;电阻读数 102 > 10 00 1k 473 > 47 000 2、Keil的使用 1、新建工程&#xff1a;Project > New Project Ctrl Shift N &#xff1a;新建文件夹 2、选型号&#xff1a;Atmel-AT89C52 3、xxx…

【Linux】Linux网络总结图详解

网络是进行分层管理的应用层HTTPHTPPS 传输层&#xff08;UDP、TCP&#xff09;UDPTCPTCP和UDP对比 网络层IP 数据链路层&#xff08;MAC&#xff09;/物理层&#xff08;以太网&#xff09;以太网通信&#xff08;负责网卡之间&#xff09; 网络是进行分层管理的 应用层 HTTP…

幂等性设计,及案例分析

一、redis锁处理幂等性失效 上面代码中&#xff0c;锁起不了作用&#xff1b; ——count方法&#xff0c;和insert方法在同一事务中&#xff0c;事务中包含锁&#xff0c;锁没有作用&#xff0c;锁的范围内&#xff0c;事务没提交&#xff0c;但释放锁后&#xff0c;事务提交前…

云安全与容器安全: 探讨在云环境和容器化应用中如何保护数据和工作负载的安全。

在当今数字化时代&#xff0c;云计算和容器化应用已经成为了企业业务的主要组成部分。这两项技术的普及&#xff0c;极大地提高了开发和部署的效率&#xff0c;但也带来了新的安全挑战。在本文中&#xff0c;我们将探讨云安全和容器安全的重要性&#xff0c;以及如何有效地保护…

WordPress外链页面安全跳转插件

老白博客我参照csdn和腾讯云的外链跳转页面&#xff0c;写了一个WordPress外链安全跳转插件&#xff1a;给网站所有第三方链接添加nofollow标签和重定向功能&#xff0c;提高网站安全性。插件包括两个样式&#xff0c;由于涉及到的css不太一样&#xff0c;所以分别写了两个版本…

Spring MVC (Next-1)

1.Restful请求 restFul是符合rest架构风格的网络API接口,完全承认Http是用于标识资源。restFul URL是面向资源的&#xff0c;可以唯一标识和定位资源。 对于该URL标识的资源做何种操作是由Http方法决定的。 rest请求方法有4种&#xff0c;包括get,post,put,delete.分别对应获取…

低代码可视化逻辑编排工具:JNPF

目录 Intro 一、是什么&#xff1f; 提供自动化的解决方案 二、为什么受欢迎&#xff1f; JNPF自身特点——安全、方便、高效、低耗 对于企业&#xff0c;更“安全” 成本“最低”&#xff0c;效率“最高” 三、JNPF开发平台功能展示 技术介绍 参考地址 近几年&#xff0c;随着…

Tomcat下载地址(详细)

Apache Tomcat - Apache Tomcat 8 Software Downloadshttps://tomcat.apache.org/download-80.cgi2.找到Archives 3.选择下载的把版本 4.选择具体下载那个版本 5. 6.一般选择tar.gz结尾的压缩包

飞桨国际化应用案例:挪威广告企业Adevinta应用PaddleOCR提质增效

Adevinta&#xff0c;位于挪威奥斯陆的跨国在线分类广告公司&#xff0c;以其全球市场的图像处理API为特色。Adevinta的主要使命是构建全球买家和卖家之间的桥梁&#xff0c;其在线市场运营覆盖11个国家&#xff0c;拥有众多备受信任的品牌&#xff0c;如荷兰的marktplaats、德…

Stream 流对象的创建与各方法

Stream 流对象的创建与各方法 目录 1.0 Stream 流的说明 2.0 Stream 流对象的创建 2.1 对于 Collection 系列集合创建 Stream 流对象的方式 2.2 对于 Map 系列集合创建 Stream 流对象的方式 2.3 对于数组创建 Stream 流对象的方式 3.0 Stream 流的中间方法 3.1 Stream 流的 …

Jupyter notebook如何加载torch环境

默认你已经安装了anaconda 和 pytorch 环境。 1&#xff0c;必须要以管理员身份打开 Anaconda prompt终端&#xff0c; 2&#xff0c;进入pytorch环境中&#xff1a; conda activate pytorch_393&#xff0c;安装必要插件&#xff1a; &#xff08;1&#xff09;conda inst…

【力扣】2003. 每棵子树内缺失的最小基因值

【力扣】2003. 每棵子树内缺失的最小基因值 文章目录 【力扣】2003. 每棵子树内缺失的最小基因值1. 题目介绍2. 思路3. 解题代码4. Danger参考 1. 题目介绍 有一棵根节点为 0 的 家族树 &#xff0c;总共包含 n 个节点&#xff0c;节点编号为 0 到 n - 1 。 给你一个下标从 0…

国密SM算法及实现加密和解密

一 引入pom <dependency><groupId>com.antherd</groupId><artifactId>sm-crypto</artifactId><version>0.3.2</version></dependency> 二 代码实现 package com.example.ytyproject.component;import com.antherd.smcrypto.…

[架构之路-250/创业之路-81]:目标系统 - 纵向分层 - 企业信息化的呈现形态:常见企业信息化软件系统 - 企业内的数据与数据库

目录 一、数据概述 1.1 数据 1.2 企业信息系统的数据 1.3 大数据 1.4 数据与程序的分离思想 1.5 数据与程序的分离做法 1.6 数据库的基本概念 1.7 企业数据来源 1.8 企业数据架构 二、常见的数据库类型 2.1 数据库分类 2.1 数据库类型 2.2 常见的数据库类型、应用…

在前端实现小铃铛上展示消息

点击铃铛显示如下消息框&#xff1a; 如果点击消息&#xff0c;可以实现消息从列表中移除,并从铃铛总数上进行扣减对应的已读消息数。 关于以上功能的实现方式&#xff1a; <!-- 铃铛位置 --><i class"el-icon-bell" click"showPopover true"&…

torch_geometric,scatter,sparse, cluster的安装失败

首先&#xff0c;对于自己的电脑环境是 已将安装3.9版本的python&#xff0c;成功安装11.6版本的cuda和1.31.1版本的torch。 现在想要安装torch_geometric&#xff0c; -需要先安装scatter&#xff0c;sparse&#xff0c; cluster。 直接安装失败&#xff0c;报错如下&…

小程序https证书

小程序通常需要与服务器进行数据交换&#xff0c;包括用户登录信息、个人资料、支付信息等敏感数据。如果不使用HTTPS&#xff0c;这些数据将以明文的方式在网络上传输&#xff0c;容易被恶意攻击者截获和窃取。HTTPS通过数据加密来解决这个问题&#xff0c;确保数据在传输过程…

make工具的介绍,包含的显示/隐晦规则/变量定义/文件指示,使用,.PHONY的介绍+原理

目录 make--自动化构建工具 引入 介绍 包含 显式规则 隐晦规则 变量定义 文件指示 注释 使用 test:test.c .PHONY 介绍 作用 示例 原理 示例 介绍 make--自动化构建工具 引入 在软件开发过程中&#xff0c;通常需要编译、链接和构建大量的源代码文件如果全…