归并排序+非比较排序

Hello everyone!欢迎来到排序章节目前的“终章”——归并排序,经过了前面三种排序的敲打,尤其是快速排序,相信你一定可以闯过这最后一关!

归并排序

基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

在这里插入图片描述
可以看到步骤是这样的:首先把原来的数据对半分开,然后继续对半分,直到一个部分只剩下一个元素;然后,在合并的过程中,一个合成两个,两个合成四个,在合成的过程中都变的有序,最后拼接在一起。

归并排序的特性总结:

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

归并排序中,临时数组(也被称为辅助数组)的作用是用于临时存储排序过程中的中间结果。当归并排序对数组进行排序时,它需要额外的空间来临时存放排序的结果,以便将排好序的子数组合并为更大的数组。

具体来说,在归并排序的合并阶段,临时数组用于暂时存储已排好序的子数组的元素,然后再将排好序的子数组合并为更大的有序数组。

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	// 递归退出条件
	if (begin >= end)
		return;

	// 求中点
	int mid = (begin + end) / 2;

	// 递归排序左右两部分
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	// 合并两个有序数组
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		// 按顺序将较小的元素放入临时数组中
		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++];
	}

	// 将排好序的临时数组拷贝回原数组
	memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

// 归并排序
void MergeSort(int* a, int n)
{
	// 分配临时数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	// 调用递归排序函数
	_MergeSort(a, 0, n - 1, tmp);

	// 释放临时数组空间
	free(tmp);
}

归并排序(非递归版本)

有些同学看到非递归,也许会想到在快排中我们使用了栈作为模拟递归的容器,于是认为我们在这里也会使用栈来用。可是在这里会出现一个问题,那就是当我们分解到只有一个数的时候,其是无法进行排序返回的,不像递归,所以会造成原数组后面的一部分被随机值占据。在这里我们要用到的是什么呢,答案是循环

我们设定一个值叫gap(步长),表示为2*gap个元素归并为1组,例如gap=1就是2个元素归并为一组,gap=2就是4个元素归并为一组。我们不需要将其拆解,而是在一开始就把它们当成独立的个体去归并,就像这样

在这里插入图片描述
比如gap=1时,我们就会得到6 10 1 7 3 9 2 4的返回结果,而gap=2时,就会得到1 6 7 10 2 3 4 9的结果,直到gap=4时最终排序结束得到结果

这样写的话,我们就能得到这样的结果:

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 j = begin1;
			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++];
			}

			memcpy(a + i, tmp + i, sizeof(int) * 2 * gap);
		}
		gap *= 2;
	}
	free(tmp);
}

这样写,对于我们刚才的数组来说,运行起来排序是相当可以的,可是,我们如果把数组的个数改变,改成不是2的次方的个数,我们就会发现,程序崩溃了,这是为什么呢?举个例子给大家看看
我们先创建这样一个数组:10 8 7 1 3 9 4 2 9 10,这里面包含了10个元素,显然不是2的次方个数,然后我们再对部分代码进行修改:

while (gap < n) {
		printf("gap:%d->",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;
			//[begin1,end1][begin2,end2]归并
			printf("[%d %d][%d %d]",begin1,end1,begin2,end2);

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

				else {
					tmp[j++] = a[begin2++];
				}
			}

当我们修改了代码之后,再次运行程序,就会输出这样的语句:
在这里插入图片描述
我们发现,标红的地方都有数组的越界,也难怪我们无法给其排序,并且造成程序的崩溃,那么,面对这些问题,我们要怎么处理呢?

我们会发现,除了begin1,end1、begin2、end2都有可能越界。面对end1,其越界证明前面的元素已经排序好了,后面没有数据可以处理,所以可以直接退出此次循环;而begin2也是一样的,前面的数据已经排好了;而end2不同,我们需要将其限制范围为n-1 。修改这些之后,也别忘了将memcpy的内存范围进行修改,更改为end2-i+1
代码如下:

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;

			// 处理边界情况
			if (end1 >= n || begin2 >= n) {
				break;
			}

			// 处理右边子数组越界情况
			if (end2 >= n) {
				end2 = n - 1;
			}

			// 归并排序合并操作
			int j = begin1;
			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++];
			}

			// 将排好序的临时数组拷贝回原数组
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;  // 增加步长进行下一轮合并
	}
	free(tmp);  // 释放临时数组内存
}

归并排序的应用

我们知道,排序分为外排序和内排序,外排序就是在硬盘(文件)中进行排序,而内排序就是在内存中进行排序。我们之前学过的如插入排序,交换排序等等一系列都是内排序,但唯独归并排序既可以作为内排序,还可以用作外排序。

我们知道,内存的特点就是小/快/价格规格贵/带电存储,而硬盘则是大/慢/价格规格便宜/不带电存储,所以当我们想要排序文件中的庞大数据时,我们必定会用到归并排序

假设我们电脑的内存为4G,而现在我需要排序一个16G的文件,这个时候,我们就会每次读4G数据到内存中用之前我们学过的如希尔排序、堆排序、快速排序等等方法对其进行排序,(fscanf)写到一个小文件中去;而这样写了四个文件后,它们就可以两两归并(fprintf),再两两归并,最终合成一个有序的16G大文件

如果笔者日后有时间,会为大家在以后补充这种这段应用的代码供大家参考,期望大家的关注!

非比较排序

刚才我们学习的,全都是需要比较才可以使用的排序方法,而这时候我们要学习的一种方法叫做非比较排序。大家第一次听一定会觉得很奇怪,什么?不比较也能知道各个元素的顺序吗?等下就为大家揭开非比较排序的原理!

非比较排序中大家听的比较多的是基数排序,计数排序和桶排序,但在这篇文章中,我们只讲计数排序

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:

  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中

在这里插入图片描述
就比如我们要排序一个1 3 9 1 5 1 2 3的数组,我们创建一个count数组来记录其每个值的出现次数,并在后面遍历count数组的时候直接把值输出,从最小值到最大值。因为数组的排序本身从小到大的,所以输出的时候顺序也是正序的。但这里的数很小很小,试问如果我们要排序1000~1999的值,我们不可能按照其真实值开辟一个2000int型大小的空间,那样前面的空间就会造成浪费,而这样子的方法也叫做绝对映射,也就是值对应的下标就是其值的大小;为了防止这种浪费,我们会使用相对映射,范围大小为max-min,而count的下标就设置成这个样子:count[a[i]-min],这样就可以保证,数组依然是从零开始存储,不会浪费空间,代码如下:

//计数排序
//时间:O(N+range)
//空间:O(range)
//相对映射解决了负数的问题同时,这个大家可以自己带值进去看
void CountSort(int* a, int n) {
	int min = a[0], max = a[0];
	for (int i = 0; i < n; i++) {
		if (a[i] > max)
			max = a[i];

		if (a[i] < min)
			min = a[i];
	}

	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));//calloc创造时所有元素置为0
	if (count == NULL) {
		perror("calloc fail");
		return;
	}

	//统计次数
	for (int i = 0; i < n; i++) {
		count[a[i] - min]++;//相对位置奥
	}

	//排序
	int i = 0;
	for (int j = 0; j < range; j++) {
		while (count[j]--) {
			a[i++] = j + min;//把值还原咯
		}
	}
}

并且我们可以发现,在N和range的大小相似时,这个排序甚至可以近似于O(N)的时间复杂度,在某些特定情况下发挥超常,平时我们一般用的还是我们之前讲到的几种排序

计数排序的特性总结:

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  2. 时间复杂度:O(MAX(N,范围))
  3. 空间复杂度:O(范围)
  4. 稳定性:稳定

排序总结

经过之前的选拔,你已经成功通过了排序闯关,接下来看看总结,看看你有没有忘记什么(doge)

①直接插入排序:直接插入排序是一个一个从后面插入,并与前面的值比较,逆序的时候(最差情况),要排升序就需要从头到尾都要移动,逐次移动1+2+3+…n-1次,是一个标准的等差数列,所以时间复杂度O(n^2);没开额外空间,所以空间复杂度O(1);

  • 说到稳定性,在这里先要提醒一下大家,这可指的不是性能的波动,而是数组里面有相同的值,保证其相对顺序不变就是稳定的。就比如一个5在另一个5后面,在排序之后还能保持这1个5在另一个5的后面,那就说明其是稳定的。
  • 举个例子,如果我们要给同样排名的同学发奖品,可是排名数相同的同学太多了,我们这个时候就按照一开始的交卷顺序来看,那我们肯定不希望自己原本在可以拿到奖品的位置,后面却被别人顶替掉了,这个时候稳定性就十分重要了。
  • 再延申出来一个例子,假如我们给结构体排序。比如我们超市要上架一批同样效果的竞争产品,但其评分不同,给超市的佣金也不同,这时候我们分高的依然在前面,但是评分相同的就要看谁给的佣金高,这怎么办呢?像下面这么做就可以啦。用金额排完降序之后,给钱高的必然排在前面,而后面再用稳定的排序对评分进行排序,9.0这种评分相同的原先按照金额排好的顺序就不会变。
    在这里插入图片描述
    ②希尔排序:由于希尔排序先分组预排,再最后进行直接插入排序,无法准确的求出其精确的时间复杂度,所以直接按照O(N^1.3)来记;而空间复杂度自然也是O(1);稳定性:不稳定,预排序中,相同的值可能分在不同组,无法控制,举个例子:1 5 3 4 4 5 6 7 ,gap=3,一组是1 4 6,一组是5 4 7,一组是3 5,那么排序完就是1 4 6,4 5 7,3 5,顺序就会变成1 4 3 4 5 5 6 7,第二组中的4原本再第一组的后面,现在就蹦到了前面。
    ③选择排序:选择排序的方法是依次选最大的数放在左边,最小的数放在右边,然后交换,本质上时间复杂度依然是等差数列,O(N^2);空间O(1);稳定性:选择排序看似稳定,但实际上我们模拟一个情景,9…9…5…5,假设9就是最大的数,那么我们把右边的9跟最右边的5交换,两个9的相对位置没有变,但是两个5的相对位置很明显发生了变化,所以并不稳定
    ④堆排序:时间复杂度O(N*logN),在堆排序中,建堆操作的时间复杂度为 O(n),而排序过程中涉及的比较和交换的次数也是 O(n log n);空间复杂度O(1);稳定性:不稳定,就比如2 2 2 1 0这个数组,建堆时会生成这样的堆。在这里插入图片描述
    于是,当开始排序的时候,顶上的2就会与0换位置,相对位置就会明显发生改变。
    ⑤冒泡排序:时间复杂度O(n^2),标准的等差数列;空间复杂度O(1);稳定性:稳定,毕竟遇到相同大小的并不会往后交换
    ⑥快速排序:时间复杂度O(nlogn),在最好的情况下,快速排序的时间复杂度是 O(n log n)。这是当每次递归调用都能将数组大致分成两半时的情况。在最坏的情况下,快速排序的时间复杂度是 O(n^2)。这通常发生在输入数组已经排序或接近排序时。而空间复杂度的计算,我们在快速排序中,递归的深度通常是logn,我们在递归时,递归完左边的,递归右边时空间会重复利用,并且空间并不会累积,空间复杂度不看累计看深度,故其空间复杂度为O(logn);稳定性:不稳定,因为虽然比key大的值在右边,比key小的值在左边,但是相等的值既可能在左边也可能在右边,并且我们用三数取中优化算法的时候,就已经有可能出现替换了
    ⑦归并排序:时间复杂度O(nlogn);空间复杂度O(n),因为我们开辟了n个临时空间;稳定性:稳定,但是要在原有的基础上将小于改成小于等于
    eg.nlogn时间复杂度的逻辑都是二分的逻辑思想

从这里开始排序这个栏目就正式结束了,但我们的学习并不会在这里终止,往后的路是星辰大海!欢迎来到下一个栏目,C嘎嘎!大家多多关注啦!

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

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

相关文章

Idea编写mapper.xml文件提示表名和字段

一、连接database 二、setting- > language -> sql Dialects中 的选项设为 mysql就可以了 三、测试

CS144--Chapter0--wsl2+docker环境搭建

我的笔记本配置 荣耀magicbook16&#xff0c;容量是500G&#xff0c;芯片是R7-5800 由于笔记本容量较小&#xff0c;因此考虑这个方案&#xff0c;对于台式机用户&#xff0c;建议可以直接用虚拟机或者双系统。 前言 斯坦福官网给出的方法是用他们的镜像&#xff08;基于Ubu…

Android 12 系统开机动画

修改Android开机动画有两种方式 方式一、通过adb 命令来修改&#xff1a; 进入/system/media目录&#xff0c;将里面的 bootanimation.zip 文件pull出来&#xff0c;然后解压&#xff0c;替换part0和part1中的图片&#xff0c;并且根据图片大小修改文件 desc.txt 中的内容&…

跳跃表解决01背包问题

跳跃表解决01背包问题 挺有意思的题目 看算法设计与分析有跳跃点实现&#xff0c;解决空间复杂度&#xff0c;感觉好烧脑&#xff0c;就实现了一下 结果 代码 using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices.C…

第十篇【传奇开心果短博文系列】鸿蒙开发技术点案例示例:深度解读鸿蒙全场景适配

传奇开心果短博文系列 系列短博文目录鸿蒙开发技术点案例示例系列 短博文目录前言一、鸿蒙全场景适配实现介绍二、统一核心示例代码三、设备驱动框架示例代码四、统一界面框架示例代码五、自适应布局示例代码六、分布式能力示例代码七、跨平台开发示例代码八、设备能力开放示例…

AP6317 同步3A锂电充电IC 带散热 便携式设备 充电器

概述是一款面向5V交流适配器的3A锂离子电池充电器。它是采用800KHz固定频率的同步降压型转换器&#xff0c;因此具有高达92%以上的充电效率&#xff0c;自身发热量极小。包括完整的充电终止电路、自动再充电和一个度达1%的4.2V预设充电电压&#xff0c;内部集成了防反灌保护、输…

使用ChatGPT学习大象机器人六轴协作机械臂mechArm

引言 我是一名机器人方向的大学生&#xff0c;近期学校安排自主做一个机器人方面相关的项目。学校给我们提供了一个小型的六轴机械臂&#xff0c;mechArm 270M5Stack&#xff0c;我打算使用ChatGPT让它来辅助我学习如何使用这个机械臂并且做一个demo。 本篇文章将记录我是如何使…

jsp 产品维修管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 产品维修管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql5.…

【51单片机系列】应用设计——8路抢答器的设计

51单片机应用——8路抢答器设计 文章设计文件及代码&#xff1a;资源链接。 文章目录 要求&#xff1a;设计思路软件设计仿真结果 要求&#xff1a; &#xff08;1&#xff09; 按下”开始“按键后才开始抢答&#xff0c;且抢答允许指示灯亮&#xff1b; &#xff08;2&…

2024年第七届亚洲能源与电气工程会议 (ACEEE 2024)

2024年第七届亚洲能源与电气工程会议 (ACEEE 2024)将于2024年7月20-22日在中国成都举行。本次会议由电子科技大学主办&#xff0c;电子科技大学机械与电气工程学院承办。ACEEE 2024旨在为推动能源与电气工程领域科学研究的发展&#xff0c;增进各国学者之间的学术交流&#xff…

备战蓝桥杯---数据结构与STL应用(进阶1)

让我们先来看一看map的基础应用吧&#xff1a; 下面是实现代码&#xff1a; #include<bits/stdc.h> using namespace std; typedef map<int,multiset<int> > line; map<int,multiset<int> >mx; map<int,multiset<int> >my; int n,m…

《Docker技术革命:从虚拟机到容器化,全面解析Docker的原理与应用-上篇》

文章目录 Docker为什么会出现总结 Docker的思想Docker历史总结 Docker能干嘛虚拟机技术虚拟机技术的缺点 容器化技术Docker和虚拟机技术的区别 Docker概念Docker的基本组成镜像&#xff08;image)容器&#xff08;container&#xff09;仓科&#xff08;repository&#xff09;…

Vulnhub靶机:hackme2-DHCP

一、介绍 运行环境&#xff1a;Virtualbox(攻击机)和VMware(靶机) 攻击机&#xff1a;kali&#xff08;192.168.56.106&#xff09; 靶机&#xff1a;hackme2-DHCP&#xff08;192.168.56.107&#xff09; 目标&#xff1a;获取靶机root权限和flag 靶机下载地址&#xff1…

【lesson31】MySQL视图

文章目录 视图介绍基本使用创建视图案例删除视图 视图规则和限制 视图介绍 视图是一个虚拟表&#xff0c;其内容由查询定义。同真实的表一样&#xff0c;视图包含一系列带有名称的列和行数据。视图的数据变化会影响到基表&#xff0c;基表的数据变化也会影响到视图。 基本使用…

GitLab16.8配置webhooks、Jenkins2.4配置GitLab插件实现持续集成、配置宝塔面板实现持续部署(其三)

看本篇文章的前提是已经部署完GItlab和Jenkins服务器&#xff0c;已经可以手动构建成功&#xff0c;并且经过了很多次实践&#xff0c;对这两款软件基本熟悉。 建议大家按以下顺序看 前端自动化&#xff08;其一&#xff09;部署gitlab 前端自动化&#xff08;其二&#xff0…

百无聊赖之JavaEE从入门到放弃(十四)异常

目录 一.异常机制 二.异常分类 三.异常的处理方式 1.捕获异常(try-catch-finally) 2.声明异常&#xff08;throws 子句&#xff09; 四.try-with-resource 五.自定义异常 六.IDEA 调试 debug 一.异常机制 工作中&#xff0c;程序遇到的情况不可能完美。比如&#xff1a…

Zabbix数据库分离与邮件报警

基础环境&#xff1a;要有zabbix服务端与被监控端实验目标&#xff1a;源数据库与服务端存放在一台服务器上&#xff0c;分离后源数据库单独在一台服务器上&#xff0c;zabbix服务端上不再有数据库。环境拓扑图&#xff1a; 实验步骤&#xff1a; 1.在8.7服务器上安装相同版本…

单片机驱动多个ds18b20

目录 1设计内容 2ds18b20介绍 2.1传感器引脚及原理图 2.2寄存器配置 3程序实现 3.1配置初始化 3.2配置寄存器 3.3ROM读取 3.4温度读取 1设计内容 通过51单片机&#xff0c;读取总线上挂载的多个ds18b20的温度信息。 如下图&#xff0c;成功读取到3路温度数据。 2ds18…

MD5算法:高效安全的数据完整性保障

摘要&#xff1a;在数字世界中&#xff0c;确保数据完整性和安全性至关重要。消息摘要算法就是一种用于实现这一目标的常用技术。其中&#xff0c;Message Digest Algorithm 5&#xff08;MD5&#xff09;算法因其高效性和安全性而受到广泛关注。本文将详细介绍MD5算法的优缺点…

双屏联动系统在展厅设计中的互动类型与效果

随着各项多媒体技术的快速发展&#xff0c;让展厅中的各类展项得到技术升级&#xff0c;其中作为电子设备中最基础的显示技术&#xff0c;不仅优化了内容的展示质量&#xff0c;还实现了更具互动性的创新技术&#xff0c;如双屏联动系统就是当前展厅设计中最常见的技术类型之一…