数据结构与算法—归并排序计数排序

news2024/11/17 3:52:00

目录

一、归并排序

1、主函数

2、递归实现

3、优化递归 

4、非递归实现

5、特性总结:

二、计数排序

1、代码:

2、特性总结:

三、各种排序稳定性总结


一、归并排序

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

偶数个元素的归并逻辑图: 

 

 奇数个元素的归并动图:

这里谈到元素的偶数奇数个数,我们在代码中会讲解如何处理。

我们先从偶数个元素的数组讲解 :

 1、主函数

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

我们的思路是取出数组元素,排序后插入创建的 tmp数组中,全部有序后将tmp数组拷贝给原数组。 

  • 主函数接受两个参数,一个整数数组a和一个整数n,n 表示数组的长度。
  • MergeSort 函数首先为tmp数组开辟待空间。
  • 调用_MergeSort函数进行排序。
  • 释放tmp的空间。

2、递归实现

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

我们先看函数是如何比较每部分的: 

  • 首先计算中间位置mid,并递归地对数组的两部分进行排序。这是分治的思想,将大问题分解成小问题,使用四个指针begin1begin2、end1end2,分别指向两个部分的开始位置和结束位置,
  • 然后看三个while循环的比较插入过程,每次分割后两部分分别从头开始比较,把较小的插入tmp数组,某一部分的数全部插入数组后,结束第一个while循环。继续检查哪个数组还有剩余元素,剩下的都是较大的,直接插入tmp数组中。

下面以数组{1,6,7,10,2,3,4,9}进行比较插入:

递归思路 :

接下来,我们需要从最小的子序列到最大依次往上进行排序插入,所以这里引用递归的思想完成排序:

  • 在函数_MergeSort中,首先判断begin是否等于end,如果相等,则当前子序列只有一个元素,不需要排序,直接返回。
  • 如果不相等,则计算中间位置mid,然后递归调用_MergeSort函数对左半部分和右半部分进行排序。在排序完成后,将左半部分和右半部分合并成一个有序数组tmp。

if (begin == end)
		return;
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

 每层递归排序后,使用memcpy函数将临时数组tmp中的元素复制回原数组a中。

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

3、优化递归 

先观察一下哪里做了优化 ?

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin == end)
		return;

	if (end - begin + 1 < 10)
	{
		InsertSort(a+begin, end - begin + 1);
		return;
	}

	int mid = (begin + end) / 2;
	// [begin, mid] [mid+1, end]
	_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));
}

通过观察发现,这个递归实现与刚才的多了一个“插入排序”实现小区间优化,我们来看看它有什么用处: 

	// 小区间优化
	if (end - begin + 1 < 10)
	{
		InsertSort(a+begin, end - begin + 1);
		return;
	}

我们借助例子进行分析: 

假如我们有10000个待排序的数据,每次通过递归依次往下调用 ,这样会调用很多次函数。

我们可以将分割到数据总数较小时,调用插入排序进行辅助处理,不再递归处理,下面来一一解释 :

当数组元素总数为10时,会向下递归调用三层。

 通过二叉树的学习,我们可以借助二叉树知识来理解如何提高效率:

 用插入排序处理元素总数为10的情况,就是处理递归的倒数三层,通过二叉树的节点数计算可以得知函数调用次数,由图可知:最后三层占据87.5%的调用次数,我们解决这三层实现了递归的优化,即对元素总数为10的情况插入排序。

4、非递归实现

通过gap控制归并的子数组大小实现非递归的归并排序

 我们可以先将gap初始化为1,然后每次将gap乘以2,直到gap大于等于数组的长度为止。在每次循环中,我们将数组分成若干个大小为gap的子数组,然后对每个子数组进行排序和合并。这样,我们就可以通过循环来实现归并排序,而不需要使用递归。

在非递归中有些尾部的特殊情况,代码的修正部分进行了处理,现在让我们进入代码的讲解: 

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);

	// 1  2  4 ....
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
		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;
			}

			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);
}
  1. 首先,代码中定义了一个临时数组 tmp,用于存储归并排序中的中间结果。然后,通过一个 while 循环,不断增加 gap 的值,每次将数组分成若干个长度为 gap 的子数组,对每个子数组进行归并排序。
  2. 然后解决数组越界问题
  3. 第一种方法:每个合并段进行局部复制,
    1. 第一种情况一组的第二部分全部越界,第一部分部分越界,则不进行排序归并,有效的元素留到恰当的gap分组进行归并排序也就是当 end1 或 begin2 超出数组 a 的范围时,需要退出循环;
    2. 第二种情况一组部分未越界,另一部分部分越界,则将未越界部分,也就是当 end2 越界而 begin2 未越界时,需要将 end2 修正为 n-1。
  4. 在每个子数组中,通过一个 for 循环,将子数组分成两个部分,分别为 [begin1, end1] 和 [begin2, end2]。然后,通过两个 while 循环,将这两个部分中的元素按照从小到大的顺序合并到 tmp 数组中。
  5. 内层三个while循环结束后,通过 memcpy 函数将 tmp 数组中当前合并完成的元素拷贝回原数组 a 中,防止覆盖原数组丢失数据,因为tmp数组还有不符合归并要求的数据位置。

除了上诉讲解中的处理数组越界的方法,还有第二种方法 

第二种: 每轮合并后进行全局复制

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;
}
  1. end1 begin2 end2越界,则将第一部分中未越界的元素参与排序归并,即 end1 修正为 n-1,对于第二部分越界的,我们不需要处理,所以将begin2赋值为 n ,end2 赋值为 n-1,这样这部分为不存在的区间,不满足排序要求,不会进行处理。
  2. begin2 end2越界,只需将第二部分越界的begin2赋值为 n ,end2 赋值为 n-1,这样这部分为不存在的区间,不满足排序要求,不会进行处理。
  3. 当 end2 越界而 begin2 未越界时,需要将 end2 修正为 n-1。
  4. 最后注意将 memcpy 函数放在 for 循环结束后。 
    memcpy(a, tmp, sizeof(int) * n);

5、特性总结:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题,思路:外存大数据排序通常需要将数据分成多个小块,每个小块可以在内存中进行排序,然后将排序好的小块写入外存中。接着,我们可以将多个排序好的小块进行归并排序,得到最终的有序序列。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定 

二、计数排序

计数排序是一种常见的排序算法,也被称为鸽巢原理。它是对哈希直接定址法的变形应用。

该算法的操作步骤如下:

  1. 统计相同元素出现的次数,将其存储在一个计数数组中。
  2. 根据计数数组中的统计结果,将序列中的元素回收到原来的序列中。

计数排序的优点是速度快,适用于数据范围比较小的情况。同时,该算法不需要比较元素的大小,因此在某些情况下比其他排序算法更加高效。如果需要对大量数据进行排序,可以考虑使用其他更加高效的排序算法。

1、代码:

void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}

		if (a[i] > max)
		{
			max = a[i];
		}
	}
	int range = max - min + 1;
	int* countA = (int*)malloc(sizeof(int) * range);
	memset(countA, 0, sizeof(int) * range);

	// 统计次数
	for (int i = 0; i < n; i++)
	{
		countA[a[i] - min]++;
	}

	// 排序
	int k = 0;
	for (int j = 0; j < range; j++)
	{
		while (countA[j]--)
		{
			a[k++] = j + min;
		}
	}
}
  • 首先,代码中通过遍历数组找到了数组中的最小值和最大值,以便后面确定计数数组的大小和范围。
  • 接着,代码中动态分配了一个大小为 range 的计数数组 countA,并将其初始化为 0。
  • 然后,代码中遍历原始数组 a,统计每个元素出现的次数,并将其存储在计数数组 countA 中。
  • 最后,代码中遍历计数数组 countA,将排序后的元素重新存储回原始数组 a 中。
    • 具体来说,从计数数组的第一个元素开始遍历,如果该元素的计数值不为 0,则将其对应的元素值(即 j + min)存储到原始数组 a 的第 k 个位置上,并将 k 向后移动一位。这样,就可以将所有元素按照从小到大的顺序重新存储到原始数组 a 中,从而完成了排序。

需要注意的是,该算法的时间复杂度为 O(n + range),其中 range 表示计数数组的大小,因此当 range 比较大时,该算法的效率会变得比较低。此外,该算法只适用于元素值范围比较小的情况,如果元素值范围很大,推荐使用其他排序算法。

2、特性总结:

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

三、各种排序稳定性总结

稳定性: 假定在待排序的记录列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i] = r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的:否则称为不稳定的。

 比如上面的情况,如果排序结束后,保证 5 在 5 前面,那这个排序就是稳定的,否则就是不稳定的。

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

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

相关文章

模型部署:量化中的Post-Training-Quantization(PTQ)和Quantization-Aware-Training(QAT)

模型部署&#xff1a;量化中的Post-Training-Quantization&#xff08;PTQ&#xff09;和Quantization-Aware-Training&#xff08;QAT&#xff09; 前言量化Post-Training-Quantization&#xff08;PTQ&#xff09;Quantization-Aware-Training&#xff08;QAT&#xff09; 参…

【Proteus仿真】【51单片机】多路温度控制系统

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真51单片机控制器&#xff0c;使用按键、LED、蜂鸣器、LCD1602、DS18B20温度传感器、HC05蓝牙模块等。 主要功能&#xff1a; 系统运行后&#xff0c;默认LCD1602显示前4路采集的温…

JavaScript_动态表格_删除功能

1、动态表格_删除功能 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>动态表格_添加和删除功能</title><style>table{border: 1px solid;margin: auto;width: 100%;}td,th{text-align: …

总结:利用JDK原生命令,制作可执行jar包与依赖jar包

总结&#xff1a;利用JDK原生命令&#xff0c;制作可执行jar包与依赖jar包 一什么是jar包&#xff1f;二制作jar包的工具&#xff1a;JDK原生自带的jar命令&#xff08;1&#xff09;jar命令注意事项&#xff1a;&#xff08;2&#xff09;jar包清单文件创建示例&#xff1a;&a…

20231112_DNS详解

DNS是实现域名与IP地址的映射。 1.映射图2.DNS查找顺序图3.DNS分类和地址4.如何清除缓存 1.映射图 图片来源于http://egonlin.com/。林海峰老师课件 2.DNS查找顺序图 3.DNS分类和地址 4.如何清除缓存

winform+access数据库增删查改报表导出demo源码

C#winformaccess数据库增删查改报表导出demo源码设备管理的一个简单程序使用access数据库增删查改导出报表功能 OleDbConnection conn new OleDbConnection("Data Source" System.Windows.Forms.Application.StartupPath "\\config\\cinfor.mdb;ProviderMicr…

Java图像编程之:Graphics

一、概念介绍 1、Java图像编程的核心类 Java图像编程的核心类包括&#xff1a; BufferedImage&#xff1a;用于表示图像的类&#xff0c;可以进行像素级的操作。Image&#xff1a;表示图像的抽象类&#xff0c;是所有图像类的基类。ImageIcon&#xff1a;用于显示图像的类&a…

.net在使用存储过程中IN参数的拼接方案,使用Join()方法

有时候拼接SQL语句时&#xff0c;可能会需要将list中的元素都加上单引号&#xff0c;并以逗号分开&#xff0c;但是Join只能简单的分开&#xff0c;没有有单引号&#xff01; 1.第一种拼接方案 List<string> arrIds new List<string>(); arrIds.Add("aa&qu…

微软近日限制员工访问ChatGPT!

作者 | 撒鸿宇 据CNBC报道&#xff0c;在这周四的短时间内&#xff0c;微软的员工被禁止使用ChatGPT。 微软在其内部网站的更新中表示&#xff1a;“由于安全和数据问题&#xff0c;一些AI工具不再对员工开放。”据CNBC查证&#xff0c;他们看到了一张截图&#xff0c;该截图显…

[Go语言]SSTI从0到1

[Go语言]SSTI从0到1 1.Go-web基础及示例2.参数处理3.模版引擎3.1 text/template3.2 SSTI 4.[LineCTF2022]gotm1.题目源码2.WP 1.Go-web基础及示例 package main import ("fmt""net/http" ) func sayHello(w http.ResponseWriter, r *http.Request) { // 定…

发布订阅者模式(观察者模式)

目录 应用场景 1.结构 2.效果 3.代码 3.1.Main方法的类【ObserverPatternExample】 3.2.主题&#xff08;接口&#xff09;【Subject】 3.3.观察者&#xff08;接口&#xff09;【Observer】 3.4.主题&#xff08;实现类&#xff09;【ConcreteSubject】 3.5.观察者&a…

[工业自动化-16]:西门子S7-15xxx编程 - 软件编程 - 西门子仿真软件PLCSIM

目录 前言&#xff1a; 一、PLCSIM仿真软件 1.1 PLCSIM仿真软件基础版&#xff08;内嵌&#xff09; 1.2 PLCSIM仿真软件与PLCSIM仿真软件高级版的区别&#xff1f; 1.3 PLCSIM使用 前言&#xff1a; PLC集成开发环境是运行在Host主机上&#xff0c;Host主机与PLC可以通过…

外星人笔记本键盘USB协议逆向

前言 我朋友一台 dell g16 购买时直接安装了linux系统&#xff0c;但是linux上没有官方的键盘控制中心&#xff0c;所以无法控制键盘灯光&#xff0c;于是我就想着能不能逆向一下键盘的协议&#xff0c;然后自己写一个控制键盘灯光的程序。我自己的外星人笔记本是m16&#xff…

基恩士软件的基本指令(二)

目录 基础指令 输入输出常开常闭指令 “A软元件名称--装入快捷键” “O软元件名称--输出快捷键” “ALT回车--连线快捷键” “B软元件--常闭接点” “软元件“/”--切换常开/常闭接点状态” 上升沿下降沿指令 “P-软元件回车--上升沿输入方法” “F-软元件回车--下降沿输入…

logback异步日志打印阻塞工作线程

前言 最新做项目&#xff0c;发现一些历史遗留问题&#xff0c;典型的是日志打印的配置问题&#xff0c;其实都是些简单问题&#xff0c;但是往往简单问题引起严重的事故&#xff0c;比如日志打印阻塞工作线程&#xff0c;以logback和log4j2为例。logback实际上是springboot的…

通过SD卡给某摄像头植入可控程序

0x01. 摄像头卡刷初体验 最近研究了手上一台摄像头的sd卡刷机功能&#xff0c;该摄像头只支持fat32格式的sd卡&#xff0c;所以需要先把sd卡格式化为fat32&#xff0c;另外微软把fat32限制了最大容量32G&#xff0c;所以也只能用不大于32G的sd卡来刷机。 这里使用32G的sd卡来…

flutter逆向 ACTF native app

前言 算了一下好长时间没打过CTF了,前两天看到ACTF逆向有道flutter逆向题就过来玩玩啦,花了一个下午做完了.说来也巧,我给DASCTF十月赛出的逆向题其中一道也是flutter,不过那题我难度降的相当之低啦,不知道有多少人做出来了呢~ 还原函数名 flutter逆向的一大难点就是不知道l…

RGMII回环:IDDR+ODDR+差分接口

目录 一、实验内容二、原理解释三、程序1、顶层文件&#xff1a;2、子模块2.1 oddr模块2.2、iddr顶层模块2.3、iddr子模块 3、仿真4、注意5、下载工程及仿真 一、实验内容 1、通过IDDR和ODDR的方式完成RGMII协议&#xff1b; 2、外部接口使用OBUFDS、IBUFDS转换成差分接口&…

C++语言的广泛应用领域

目录 1. 系统级编程 2. 游戏开发 3. 嵌入式系统 4. 大数据处理 5. 金融和量化分析 6. 人工智能和机器学习 7. 网络和通信 结语 C是一种多范式编程语言&#xff0c;具有高性能、中级抽象能力和面向对象的特性。由Bjarne Stroustrup于1979年首次设计并实现&#xff0c;C在…

如何确定线程栈的基址?

起 很早之前&#xff0c;我遇到过几个与栈相关的问题&#xff0c;当时总结过几篇关于线程栈的文章&#xff0c;分别是 《栈大小可以怎么改&#xff1f;》、《栈局部变量优化探究&#xff0c;意外发现了 vs 的一个 bug &#xff1f;》、《栈又溢出了》、《有趣的异常》。在这几…