基础排序算法【归并排序+非递归版本+边界修正】

news2024/12/24 20:50:26

基础排序算法【归并排序+非递归版本+边界修正】

  • Ⅰ.归并排序(递归版本)
      • ①.分割
      • ②.归并
      • ③.拷贝
  • Ⅱ.非递归版本
  • Ⅲ.边界修正

Ⅰ.归并排序(递归版本)

递归排序,采用的是分治法。分成子问题来处理。先让序列不断分割成子序列,当子序列有序后再合并。

对于一段序列,分割成左右子序列,再对左序列进行不断分割(左序列又形成左右序列……),直到无法分割为止,开始进行合并,将分割的子序列合并成有序序列。当左序列合并完后,右序列再开始不断分割,当右序列分割完进行合并,合并完,右序列也就有序了,左右序列再合并。最终形成一个有序序列。
在这里插入图片描述
请添加图片描述
递归展开图分析:
在这里插入图片描述
注意:因为合并有序序列需要用到新的数组,所以需要动态开辟一个。但不能在含递归的函数中使用,不然就不断的开辟空间了,所以需要另一个函数来作为支架,来提供temp数组。

void MergeSort(int* a, int n)
{
	int a[] = { 8,6,4,5,3,2,7,1 };
	int n = sizeof(n) / sizeof(a[0]);
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("malloc");
	}
	_MergeSort(a, 0, n - 1, temp);
}

```cpp
void _MergeSort(int* a, int begin, int end, int* temp)
{
	if (begin >= end)
		return;
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, temp);

	_MergeSort(a, mid + 1, end, temp);
	//将[begin mid] 和[mid+1 end]合并成一个有序序列
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1,end2 = end;
	int i = begin;
	//合并两个有序序列的常用方法
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			temp[i++] = a[begin1++];
		}
		else
		{
			temp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		temp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		temp[i++] = a[begin2++];
	}

	memcpy(a + begin, temp + begin, sizeof(int) * (end - begin + 1));
}

①.分割

第一步首先对该序列进行分割,从哪分割呢?当然从中间分割。
利用递归来实现不断的分割,直到无法分割为止。

void _MergeSort(int* a, int begin, int end, int* temp)
{
     1.//当无法分割时,开始返回。
	if (begin >= end)
		return;
		
	2.//进行分割
	int mid = (begin + end) / 2;
	//[begin  mid][mid+1 end]
	//不断的分割直到分成一个有序序列时
	_MergeSort(a, begin, mid, temp);
	//将左区间不断分割
	_MergeSort(a, mid + 1, end, temp);
	//将右区间不断分割

	//走到这里表明左右小序列是一个有序序列了
	//接下来就是合并有序序列了

	//将[begin mid] 和[mid+1 end]合并成一个有序序列
}

②.归并

归并思想很简单,就是让两个有序序列进行比较,较小的值先放入新开辟的数组temp里,较小数组的下标要++,然后再进行比较,重复这个操作,直到某一个序列被全部放入数组中。
接下来,肯定有一个数组没有全部放入,但不知道哪一个,需要进行讨论,直接将没有放空的序列直接尾插到数组temp后面即可。

//将[begin mid] 和[mid+1 end]合并成一个有序序列
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1,end2 = end;
	int i = begin;
	//合并两个有序序列的常用方法
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			temp[i++] = a[begin1++];//将较小的值放入temp中
		}
		else
		{
			temp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)//直接将没有放空的序列尾插到temp数组后面即可。
	{
		temp[i++] = a[begin1++];
	}
	while (begin2 <= end2)//因为不知道是哪一个没有被放空,需要讨论一下哪个序列还有数值。
	{
		temp[i++] = a[begin2++];
	}

③.拷贝

最后直接temp数组中的值再拷贝会a数组中就完成了a数组的排序。
可以使用memcpy函数进行拷贝。
memcpy(目的空间,源空间,大小);


memcpy(a + begin, temp + begin, sizeof(int) * (end - begin + 1));
//这里必须要加上begin,因为不一定从第一个位置开始,也可能从中间开始拷贝回去。

Ⅱ.非递归版本

我们可以采取非递归的方式来实现归并排序。因为我们知道归并的思想就是将多个有序序列合并成一个有序序列。
而递归实现的版本就是将没有序的序列分割成有序的序列,然后再进行合并,我们可以不用递归分割,直接就可以得到有序序列,因为一个序列肯定是有序的,所以我们直接可以从一个有序序列和一个有序序列开始合并,然后再根据得到的有序序列进行二二合并……。

在这里插入图片描述
这里的难点就在于边界的控制,怎么控制每次要合并的序列的边界呢?
这里我定义一个gap,这个变量代表着每次合并序列的个数。
找到边界的规律最好的方式是画图。
i就是每次要归并的起始位置。
因为每次归并的个数是gap个,所以i每次要跳过2gap个单位。
begin1就是i的位置,end1应该是i+gap位置,但是数组下标要减一。
i+gap表示end1后面的位置也就是begin2了。而end2其实就是下一次开始位置的前面,下一次要跳过2
gap位置,所以end2的位置为i+2*gap-1;
在这里插入图片描述

int gap = 1;
	//gap是表示每次归并的个数
		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 j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					temp[j++] = a[begin1++];
				}
				else
				{
					temp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				temp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				temp[j++] = a[begin2++];
			}
		}
	

如何控制让每次归并的个数呢?只要让gap每次两倍的增长就可以了,归并一次,gap增长一倍,当gap=n/2时,再归并就可以结束了。

int gap = 1;
	//gap是表示每次归并的个数
	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 j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					temp[j++] = a[begin1++];
				}
				else
				{
					temp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				temp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				temp[j++] = a[begin2++];
			}
		}
		gap *= 2;//每次归并完后,gap增长一倍。
	}

这只是完成归并部分,还有拷贝操作没有实现,不过先不急,我们先研究一下这个归并操作有何bug。
bug:这种归并只能对于2^次方个数有用,而对于不是2的次方时,就会存在越界,因为gap每次都是增长一倍,那么边界的范围也就确定下来了,一旦序列没有2的次方个时那么就会存在要合并的两个序列区间会越界。
我们可以演示一下打印出来看下。给9个数据。
在这里插入图片描述
在这里插入图片描述
九个数据,最大的下标也就是8。所以超过8的都会越界。
begin1是永远不可能越界的,因为begin1就是i,而i是小于n的。
end2和begin2,end2会越界。
在这里插入图片描述
这里我们采用一般的拷贝方式,就是部分拷贝,归并完后就拷贝回去,而不是当全部归并后再拷贝回去。
存在三种情况:
1.当end1越界时,end1后面的肯定都越界了。那后面的我们就不要归并了,不要归并也就意味着也不用拷贝。只需要将前面的归并然后再拷贝回去。
2.当end1没有越界,begin2越界了,跟上面类似,begin2越界了,那后面就不用再归并了,不用拷贝了。
3.当end2越界了,那我们只需要将end2修改成n-1即可。

//非递归形式的归并算法
void _MergeSortNor(int* a, int n, int* temp)
{
	
	int gap = 1;
	//gap是表示每次归并的个数
	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 j = i;
		
			if (end1 >= n )
			{
				break;
			}
			if (begin2 >= n)
			{
				break;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			printf("[%d %d][%d %d]", begin1, end1, begin2, end2);
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					temp[j++] = a[begin1++];
				}
				else
				{
					temp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				temp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				temp[j++] = a[begin2++];
			}
	
			memcpy(a + i, temp + i, sizeof(int)*(end2 - i + 1));
			//有两种拷贝方式 ,1,直接等全部归并完后一把梭哈,或者是归并完一次就拷贝回去
          //归并一部分拷贝一部分
		}
		gap *= 2;

	}

}

在这里插入图片描述

Ⅲ.边界修正

而另一种拷贝方式就是全部归并完后再拷贝回去。
这种方法很折磨人,因为涉及复杂的边界修正。这里不推荐使用。

不能像上面的直接break,一定要将数据弄下来,因为这是一把拷贝过去的,如果遇到越界的那break后,可能有的数据没有进入temp数组里面,那temp数组再拷贝给a数组,这不扯谈嘛。
要保证每次归并的数据是有效的。
所以正因为这里的拷贝是一把梭哈,所以我们必须要进行边界修正处理。当end1,begin2,或end2出现越界了,就要修改它的边界。
有人这样修改,对吗?
很明显是不对的。不可以全部都变成n-1,因为这样会将最后一个数字多归并一次。

//当end1越界时
if (end1 >= n)
{
	end1 = n - 1;
	begin2 = n - 1;
	end2 = n - 1;
}

正确的修正是让第二个区间不存在,那么下面的合并操作就不会进去。

if (end1 >= n)
{
	end1 = n - 1;
	begin2 = n;//将第二个区间修正成不存在就可以了
	end2 = n - 1;
}

当begin2越界时,前面的[begin1,end1]是正常的,要进行归并,而后面就不需要了,所以直接让后面的区间变成不存在的就可以不进入归并操作。

if (begin2 >= n)
{
	begin2 = n;//将区间修正成不存在就可以了
	end2 = n - 1;
}

当end2越界时,也就是前面的[begin1,end1],begin2没有越界,这时只需要将end2修正一下就可以了。

if (end2 >= n)
{
   end2=n-1;
}
void _MergeSortNor(int* a, int n, int* temp)
{

	int gap = 1;
	//gap是表示每次归并的个数
	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 j = i;

			if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
		    }
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			printf("[%d %d][%d %d]", begin1, end1, begin2, end2);
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					temp[j++] = a[begin1++];
				}
				else
				{
					temp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				temp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				temp[j++] = a[begin2++];
			}

		}
			memcpy(a,temp,sizeof(int)*n);
			//如果采用一把梭哈的方法,那必须采取纯修正路线
		gap *= 2;
		printf("\n");
	}

}
int main()
{
	int* temp = (int*)malloc(sizeof(int) * 9);
	if (temp == NULL)
	{
		perror("malloc");
	}
	int a[] = { 8,6,4,5,3,2,7,1, 9};
	_MergeSortNor(a, 9,temp);
	for (int i = 0; i < 9; i++)
	{
		printf("%d ", a[i]);
	}
}

在这里插入图片描述
在这里插入图片描述

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

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

相关文章

Yolov5-Face 原理解析及算法解析

YOLOv5-Face 文章目录 YOLOv5-Face1. 为什么人脸检测 一般检测&#xff1f;1.1 YOLOv5Face人脸检测1.2 YOLOv5Face Landmark 2.YOLOv5Face的设计目标和主要贡献2.1 设计目标2.2 主要贡献 3. YOLOv5Face架构3.1 模型架构3.1.1 模型示意图3.1.2 CBS模块3.1.3 Head输出3.1.4 stem…

202320读书笔记|《宋词》——竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生

202320读书笔记&#xff5c;《宋词》——竹杖芒鞋轻胜马&#xff0c;谁怕&#xff1f;一蓑烟雨任平生 《宋词》韩震主编&#xff0c;偶然从书友那加入书架的书。宋词挺喜欢李清照的词以及知否的《菩萨蛮》。诗集&#xff0c;词&#xff0c;俳句&#xff0c;短歌我都很喜欢&…

工欲善其事,必先利其器-基于ubuntu18.04 VScode开发100ASK-ESP32

点击上方“嵌入式应用研究院”&#xff0c;选择“置顶/星标公众号” 干货福利&#xff0c;第一时间送达&#xff01; 来源 | 嵌入式应用研究院 整理&排版 | 嵌入式应用研究院 前面我们基于ubuntu环境搭建了esp-idf的开发环境&#xff0c;它也是为了接下来基于VSCode来开发1…

【openGauss基本概念】---快速入门

【openGauss简单使用】---快速入门 &#x1f53b; 一、基本概念&#x1f530; 1.1 openGauss&#x1f530; 1.2 数据库&#xff08;Database&#xff09;&#x1f530; 1.3 数据块&#xff08;Block&#xff09;&#x1f530; 1.4 行&#xff08;Row&#xff09;&#x1f530; …

【MySQL数据库】主从复制与读写分离

目录 一、读写分离1.1概述1.2为什么要读写分离呢&#xff1f;1.3什么时候要读写分离&#xff1f; 1.4主从复制与读写分离1.5mtsql支持的复制类型1.6主从复制工作流程1.7主从复制原理 二、主从复制实战 一、读写分离 1.1概述 读写分离&#xff0c;基本的原理是让主数据库处理事…

React从入门到实战 -组件的三大核心属性(2)props

文章目录 基本使用对props进行限制类式组件中的构造器 基本使用 // 定义组件class MyComponent extends React.Component{render(){return (<ul><li>姓名&#xff1a;{this.props.name}</li><li>年龄&#xff1a;{this.props.age}</li><li>…

【开源与项目实战:开源实战】85 | 开源实战四(中):剖析Spring框架中用来支持扩展的两种设计模式

上一节课中&#xff0c;我们学习了 Spring 框架背后蕴藏的一些经典设计思想&#xff0c;比如约定优于配置、低侵入松耦合、模块化轻量级等等。我们可以将这些设计思想借鉴到其他框架开发中&#xff0c;在大的设计层面提高框架的代码质量。这也是我们在专栏中讲解这部分内容的原…

MongoDB负载均衡集群(第8章节选)

MongoDB自身可组成分片加复制的集群&#xff0c;在这个集群的前端加上负载均衡器&#xff08;比如HAProxmy Keepalived&#xff09;&#xff0c;就可组建成一个无单点故障、十分完美的高可用负载均衡集群&#xff08;如图8-1所示&#xff09;。 图8- 1 整个MongDB高可用体系结…

基于java+swing+mysql飞机票预订系统

基于javaswingmysql飞机票预订系统 一、系统介绍二、功能展示1.项目内容2.项目骨架3.数据库表4.注册窗口5.登录窗口6、用户-主窗口7、用户-查询航班8.用户--订票8.用户--取票9.管理员-所有航班信息10.管理员-添加航班11.用户信息12.订票状态 四、其它1.其他系统实现五.获取源码…

路径规划-DWA算法(C++实现)

1、简单介绍 DWA算法&#xff08;dynamic window approach&#xff09;&#xff0c;其原理主要是在速度空间&#xff08;v,w&#xff09;中采样多组速度&#xff0c;并模拟出这些速度在一定时间内的运动轨迹&#xff0c;并通过评价函数对这些轨迹进行评价(其中包括距离障碍物距…

【学习笔记】 科目一之概念篇

【学习笔记】 科目一之概念篇 概念题方法 1&#xff09;抓重点&#xff1a;科目一设计知识范围太广&#xff0c;不要妄想所有知识点都复习到&#xff0c;这是不可能的&#xff0c;我们的目标是45分几个而不是考高分&#xff0c;复习时间有限&#xff0c;所以要学会抓重点&…

图片是如何生成的--图像生成模型(GAN、VAE、扩散模型)简介

目录 1.GAN 2.AutoEncoder及其变种&#xff1a;AE/DAE/VAE/VQVAE 2.1 AE&#xff1a; 2.2 DAE&#xff1a;Denoising AutoEncoder 2.3 VAE&#xff1a;Variational AutoEncoder 2.4 VQVAE&#xff1a;Vector-quantised Variational AutoEncoder 3. 扩散模型 3.1 扩散…

【openGauss简单数据管理】--快速入门

【openGauss简单数据管理】--快速入门 &#x1f53b; 一、openGauss数据库管理&#x1f530; 1.1 连接openGauss数据库&#x1f530; 1.2 创建数据库&#x1f530; 1.3 查看数据库和切换数据库&#x1f530; 1.4 修改数据库&#x1f530; 1.5 删除数据库&#x1f530; 1.6 启停…

【QQ界面展示-实现自动回复 Objective-C语言】

一、刚才咱们监听键盘弹出事件,是怎么监听的, 1.监听键盘弹出事件的步骤 1)首先,在控制器的viewDidLoad方法中,创建一个NotificationCenter对象啊 2)通过center,让当前控制器的这个方法,监听这个通知, 3)然后,我们在这个通知里面,获取到键盘的Y值, 4)对我们的…

Rust 原始类型之数组array内置方法

目录 数组 array 声明 访问 引用 Reference 切片 Slice 方法 题目实例 数组 array 在 Rust 中&#xff0c;数组是一种固定大小的数据结构&#xff0c;用于存储具有相同数据类型的元素的有序集合。 “固定大小”是指数组中的元素的类型和数量确定&#xff0c;也就确定了…

【从零开始学习JAVA | 第十八篇】接口介绍

目录 前言&#xff1a; 接口&#xff1a; 如何定义一个接口&#xff1a; 如何使用一个接口&#xff1a; 接口中成员的特点&#xff1a; 接口与类的区别&#xff1a; 接口的应用&#xff1a; 总结&#xff1a; 前言&#xff1a; 接口其实是为了弥补继承的缺点&#xf…

C语言文件打开关闭详解、文件顺序读写详解。

文件的打开和关闭 fopen函数原型&#xff1a; FILE *fopen( const char *filename, const char *mode );const char *filename 文件的路径以及名字const char *mode 文件的打开方式 文件打开方式含义如果文件不存在“r”读文件不存在会报错“w”写(清空写)建立一个新的文件“…

【新手上路】如何在Web3时代成为XR创建者

目录 0 XR在Web3里的作用 1 XR的概念、特征、技术、设备、平台、应用和工具 1.1 VR的概念、特征和技术 1.2 AR的概念、特征和技术 1.2 XR的设备、平台、应用和工具 2 选择XR的方法 2.1 何时使用VR 2.2 何时使用AR 3 开发XR作品的4个步骤 4 成为XR构建者的路径 4.1 三…

小程序布局中相对定位的用法

小程序中一般为了有一定的设计效果&#xff0c;会将下边组件的内容提升一点到上边去&#xff0c;比如我们的电商展示模板里&#xff0c;会将商品列表覆盖一点到背景图&#xff0c;效果如下&#xff1a; 这种要如何搭建呢&#xff1f;就是利用到了CSS相对定位的原理 搭建组件 …

27.移除元素

LeetCode-27.移除元素 1、题目描述2、解题思路3、代码实现3.1Java代码实现3.2双指针代码优化 4、解题记录 1、题目描述 题目描述&#xff1a; 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素&#xff0c;并返回移除后数组的新长度。 不要…