【八大排序】归并排序 | 计数排序 + 图文详解!!

在这里插入图片描述

📷 江池俊: 个人主页
🔥个人专栏: ✅数据结构冒险记 ✅C语言进阶之路
🌅 有航道的人,再渺小也不会迷途。


在这里插入图片描述

文章目录

    • 一、归并排序
      • 1.1 基本思想 + 动图演示
      • 2.2 递归版本代码实现 + 算法步骤
      • 2.3 非递归版本代码实现 + 算法步骤
      • 2.4 归并排序的特性总结
    • 二、计数排序
      • 2.1 基本思想
      • 2.2 动图演示
      • 2.3 算法步骤
      • 2.4 代码实现
      • 2.5 计数排序特性总结
    • 三、排序算法复杂度及稳定性分析

在这里插入图片描述

一、归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

1.1 基本思想 + 动图演示

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

在这里插入图片描述

在这里插入图片描述

2.2 递归版本代码实现 + 算法步骤

归并排序的基本思想是分治思想,它包括以下三个步骤:

  1. 分解(Divide):将含有n个元素的序列分成两个各自包含大约n/2个元素的子序列。(当数组分解成一个时即可认为其有序)
  2. 解决(Conquer):递归地对这两个子序列进行归并排序。
  3. 合并(Combine):将两个排序好的子序列合并成一个最终的排序序列。

归并排序通过不断地将大问题分解成小问题来解决,即把大的数组拆分成若干个小的数组,然后逐一合并这些有序的小数组来得到最终排序好的整体数组。这种算法非常适用于链表等数据结构,在处理大规模数据时尤其高效。

// 归并排序递归函数
void _MergeSort(int* a, int begin, int end, int* temp)
{
	if (begin >= end)
		return;

	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]
	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 n)
{
	// 申请一个与原数组同样大小的空间
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("malloc fail");
		return;
	}

	_MergeSort(a, 0, n - 1, temp);

	free(temp);
}

【递归展开图】:
在这里插入图片描述

现在我们来分析一下以上代码:
这段代码是归并排序(Merge Sort)的实现。归并排序是一种分治算法,它将一个数组分成两半,对每一半进行排序,然后将两个有序的部分合并成一个有序的数组。以下是这段代码的算法思想和步骤分析:

  1. 递归划分

    • _MergeSort函数中,首先检查基准条件,即如果begin大于或等于end,则数组已经完全有序,所以直接返回。
    • 然后,计算中间索引mid,将数组分成两个子数组:[begin, mid][mid+1, end]
    • 对这两个子数组递归地进行归并排序。
  2. 合并

    • 在递归调用返回后,两个子数组都是有序的。然后,将这两个有序的子数组合并成一个有序的数组。
    • 合并操作通过双指针技术完成。指针begin1begin2分别指向两个子数组的开始位置,而指针end1end2分别指向两个子数组的结束位置。
    • 开始时,从两个子数组中取最小的元素,放到临时数组temp中,直到其中一个子数组被完全取完。
    • 然后,将剩余的子数组的所有元素复制到临时数组中。
  3. 拷贝回原数组

    • 最后,使用memcpy函数将临时数组中的元素复制回原数组。这一步是必要的,因为临时数组是在堆上分配的,而原数组是在栈上。
  4. 主函数

    • MergeSort函数是归并排序的入口点。它首先在堆上为原数组分配一个同样大小的临时数组。如果分配失败(即malloc返回NULL),则输出错误信息并返回。
    • 然后,调用递归函数_MergeSort对原数组进行排序。
    • 最后,释放临时数组以防止内存泄漏。
  5. 稳定性

    • 归并排序是稳定的排序算法,这意味着相等的元素在排序后保持其原始顺序。这是因为归并排序在合并两个子数组时,总是选择较小的元素,而不改变其相对顺序。
  6. 时间复杂度

    • 归并排序的时间复杂度为O(nlogn),其中n是数组的大小。这是因为每次递归调用都会将问题规模减半(logn),并且需要进行n次这样的递归调用(n)
  7. 空间复杂度

    • 归并排序的空间复杂度为O(n),因为需要一个与原数组同样大小的临时数组来存储合并过程中的中间结果。

2.3 非递归版本代码实现 + 算法步骤

// 归并排序(非递归)
void MergeSortNonR(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("malloc fail");
		return;
	}

	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;
			// [begin1, end1] [begin2, end2] 归并

			// 边界处理
			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])
				{
					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));
		}
		gap *= 2;
	}

	free(temp);
}

对于上述代码我们接着来分析一下它的算法步骤:

【算法步骤】:

  1. 初始化

    • 定义一个临时数组temp,其大小为输入数组a的大小。
    • 初始化一个变量gap为1,它表示每次归并时每组数据的个数。
  2. 归并循环

    • gap小于输入数组的长度n时,进入循环。
    • 在每次循环中,将数组分为两个子数组(每个子数组的大小为gap),并对这两个子数组进行归并。
  3. 子数组归并

    • 定义两个子数组的起始和结束索引:begin1end1begin2end2
    • 处理边界情况:如果其中一个子数组超出数组范围,则退出循环。
    • 使用一个临时数组temp来存储归并的结果。
    • 使用一个双指针方法(类似于两个指针比较和交换)来将两个子数组合并为一个有序数组。
  4. 拷贝回原数组

    • 使用memcpy函数将临时数组中的数据拷贝回原数组。这一步是为了在归并过程中更新原数组。
  5. 扩大gap

    • 在每次循环结束时,将gap乘以2,以便在下一次循环中处理更大的子数组。
  6. 释放内存

    • 归并完成后,释放临时数组temp的内存。

2.4 归并排序的特性总结

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

二、计数排序

2.1 基本思想

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

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

在这里插入图片描述

2.2 动图演示

在这里插入图片描述

2.3 算法步骤

这段代码是实现计数排序算法的C语言代码。以下是该代码的算法步骤和思想分析:

算法步骤:

  1. 找出数组中的最小值和最大值:这是计数排序的一个重要步骤,因为算法需要对数组中的每个元素进行计数,所以需要知道元素的可能范围。
  2. 计算范围:根据最小值和最大值计算出元素的可能范围。
  3. 计数:遍历输入数组,对每个元素在其可能的范围内进行计数。
  4. 构建输出数组:根据计数结果,将每个元素放到它在输出数组中的正确位置。

2.4 代码实现

// 计数排序 
// 时间复杂度:O(N+range) 空间复杂度:O(range) 
void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	for (int i = 1; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}

		if (a[i] > max)
		{
			max = a[i];
		}
	}

	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));
	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;
		}
	}
}

2.5 计数排序特性总结

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。数排序适用于整数且范围较小的情况。对于范围较大的整数或小数,需要更复杂的排序算法。
  2. 时间复杂度:O(MAX(N,范围)),由于算法只涉及到一次遍历输入数组和一次遍历计数数组,所以时间复杂度为O(MAX(N,范围))
  3. 空间复杂度:O(范围),由于需要创建一个与范围大小相等的计数数组,所以空间复杂度为O(范围)
  4. 稳定性:稳定(相等的元素在排序后保持其原始顺序)

三、排序算法复杂度及稳定性分析

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


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

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

相关文章

rediss集群 三主三从集群模式

三主三从集群模式 1)、新建redis集群目录&#xff1a;7001~7006工作目录【/app/soft/redis-cluster/目下】 2&#xff09;、在7001~7006 目录下创建bin和conf 目录&#xff0c;然后将/app/soft/redis/bin目录下的文件分别拷贝到7001~7006 目录&#xff0c;然后在7001~7006 目…

Blazor SSR/WASM IDS/OIDC 单点登录授权实例2-登录信息组件wasm

目录: OpenID 与 OAuth2 基础知识Blazor wasm Google 登录Blazor wasm Gitee 码云登录Blazor SSR/WASM IDS/OIDC 单点登录授权实例1-建立和配置IDS身份验证服务Blazor SSR/WASM IDS/OIDC 单点登录授权实例2-登录信息组件wasmBlazor SSR/WASM IDS/OIDC 单点登录授权实例3-服务端…

【制作100个unity游戏之23】实现类似七日杀、森林一样的生存游戏16(附项目源码)

本节最终效果演示 【独游开发记录】一个人开发的&#xff0c;类森林&#xff0c;七日杀生存游戏 文章目录 本节最终效果演示系列目录前言泛型单例添加声音脚步声鸭子动物音效人物各种操作音效砍树音效 效果源码完结 系列目录 前言 欢迎来到【制作100个Unity游戏】系列&#x…

106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)

题目描述 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 题目示例 输入&#xff1a;inorder [9,3,15,20,7], postorder [9,15,7,20,3] 输出&a…

Android13多媒体框架概览

Android13多媒体框架概览 Android 多媒体框架 Android 多媒体框架旨在为 Java 服务提供可靠的接口。它是一个系统&#xff0c;包括多媒体应用程序、框架、OpenCore 引擎、音频/视频/输入的硬件设备&#xff0c;输出设备以及一些核心动态库&#xff0c;比如 libmedia、libmedi…

1-3 mininet中使用python API直接拓扑定义以及启动方式对比

作为SDN网络中搭建拓扑非常重要的仿真平台&#xff0c;我们可以使用mininet默认的库内拓扑文件&#xff0c;也可以使用python语言进行自定义拓扑。使用python进行拓扑定义时&#xff0c;不同的定义方式将导致其启动的方式由所不同。 一、采用最原始的命令启动方式&#xff1a; …

MATLAB知识点:使用逻辑值修改或删除矩阵元素

​讲解视频&#xff1a;可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇&#xff08;数学建模清风主讲&#xff0c;适合零基础同学观看&#xff09;_哔哩哔哩_bilibili 节选自第3章 3.4.4 逻辑运算 3.4.4.3 使用逻辑值修改或删…

【java】简单的Java语言控制台程序

一、用于文本文件处理的Java语言控制台程序示例 以下是一份简单的Java语言控制台程序示例&#xff0c;用于文本文件的处理。本例中我们将会创建一个程序&#xff0c;它会读取一个文本文件&#xff0c;显示其内容&#xff0c;并且对内容进行计数&#xff0c;然后将结果输出到控…

Github 2024-02-09 开源项目日报 Top10

根据Github Trendings的统计&#xff0c;今日(2024-02-09统计)共有10个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量Python项目4Go项目2Scala项目1PLpgSQL项目1Ruby项目1HTML项目1Solidity项目1Lua项目1 开源个人理财应用 Mayb…

【小沐学GIS】基于Python绘制三维数字地球Earth(OpenGL)

&#x1f37a;三维数字地球系列相关文章如下&#x1f37a;&#xff1a;1【小沐学GIS】基于C绘制三维数字地球Earth&#xff08;OpenGL、glfw、glut&#xff09;第一期2【小沐学GIS】基于C绘制三维数字地球Earth&#xff08;OpenGL、glfw、glut&#xff09;第二期3【小沐学GIS】…

JavaSE——数组(2/2)-数组在计算机中的执行原理、数组案例、Debug工具的使用

目录 数组在计算机中的执行原理 Java内存分配介绍 数组的执行原理 多变量指向同一数组 数组案例 求最大值 数组反转 随机排名 Debug工具的使用 数组在计算机中的执行原理 Java内存分配介绍 程序都是在内存中执行的&#xff0c;Java程序编译之后会产生一个class文件&…

嵌入式学习之Linux入门篇笔记——17,makefile基本语法(上)

配套视频学习链接&#xff1a;http://【【北京迅为】嵌入式学习之Linux入门篇】 https://www.bilibili.com/video/BV1M7411m7wT/?p4&share_sourcecopy_web&vd_sourcea0ef2c4953d33a9260910aaea45eaec8 目录 一&#xff0e;设置 vim 首行缩进 二.Makefile 基本语法…

【JS逆向三】逆向某某网站的sign参数,并模拟生成仅供学习

逆向日期&#xff1a;2024.02.06 使用工具&#xff1a;Node.js 类型&#xff1a;webpack 文章全程已做去敏处理&#xff01;&#xff01;&#xff01; 【需要做的可联系我】 可使用AES进行解密处理&#xff08;直接解密即可&#xff09;&#xff1a;AES加解密工具 1、打开某某…

Ansible copy模块 复制文件使用 主服务器 给副服务器 复制文件使用 指定文件权限 覆盖备份等

目录 参数复制文件然后进行同时复制操作 给定内容生成文件&#xff0c;并制定权限验证 关于覆盖先查看当前内容覆盖并备份查看文件权限 还有有没有备份查看文件内容 参数 这个模块用于将文件复制到远程主机&#xff0c;同时支持给定内容生成文件和修改权限等。   其相关选项…

深入理解java之多线程(一)

前言&#xff1a; 本章节我们将开始学习多线程&#xff0c;多线程是一个很重要的知识点&#xff0c;他在我们实际开发中应用广泛并且基础&#xff0c;可以说掌握多线程编写程序是每一个程序员都应当必备的技能&#xff0c;很多小伙伴也会吐槽多线程比较难&#xff0c;但因为其实…

gem5学习(17):ARM功耗建模——ARM Power Modelling

目录 一、Dynamic Power States 二、Power Usage Types 三、MathExprPowerModels 四、Extending an existing simulation 五、Stat dump frequency 六、Common Problems 官网教程&#xff1a;gem5: ARM Power Modelling 通过使用gem5中已记录的各种统计数据&#xff0c;…

今日早报 每日精选15条新闻简报 每天一分钟 知晓天下事 2月10日,星期六

每天一分钟&#xff0c;知晓天下事&#xff01; 2024年2月10日 星期六 农历正月初一 春节 1、 国务院&#xff1a;到2025年&#xff0c;初步建成覆盖各领域、各环节的废弃物循环利用体系。 2、 国家移民管理局&#xff1a;部分国家人员可以用更多事由免签入境海南。 3、 市场…

使用AI开发一个红包封面生成器

使用 VUE3&#xff0c;和 Express 开发一个红包封面。 生成效果如下 体验地址&#xff1a;https://hongbao.digitalmodel.top/

政安晨:示例演绎TensorFlow的官方指南(三){快速使用数据可视化工具TensorBoard}

这篇文章里咱们演绎TensorFLow的数据可视化工具&#xff1a;TensorBoard。 在机器学习中&#xff0c;要改进模型的某些参数&#xff0c;您通常需要对其进行衡量。TensorBoard 是用于提供机器学习工作流期间所需测量和呈现的工具。它使您能够跟踪实验指标&#xff08;例如损失和…

Blender教程(基础)--试图的显示模式-22

一、透视模式&#xff08;AltZ&#xff09; 透视模式下可以实现选中透视的物体信息 发现选中了透视区的所有顶点 二、试图着色模式-显示网格边框 三、试图着色模式-显示实体 三、试图着色模式-材质预览 四、试图着色模式-显示渲染预览