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

在这里插入图片描述

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


在这里插入图片描述

文章目录

在这里插入图片描述

一、归并排序

归并排序(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. 稳定性:稳定(相等的元素在排序后保持其原始顺序)

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

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



http://www.niftyadmin.cn/n/5375948.html

相关文章

Linux:信号的处理

文章目录 信号处理 本篇总结的是关于信号的处理 信号处理 在之前有这样的观点&#xff1a;信号在合适的时候被处理好&#xff0c;当进程收到信号后&#xff0c;当前进程可能在做优先级更高的事&#xff0c;所以它来不及处理这个信号&#xff0c;那么就会把这个信号暂时保存起…

ARM:AI 的翅膀,还能飞多久?

ARM&#xff08;ARM.O&#xff09;于北京时间 2024 年 2 月 8 日上午的美股盘后发布了 2024 年第三财年报告&#xff08;截止 2023 年 12 月&#xff09;&#xff0c;要点如下&#xff1a; 1、整体业绩&#xff1a;收入再创新高。ARM 在 2024 财年第三季度&#xff08;即 23Q4…

vue前端RSA使用公钥进行加密,公钥进行解密

记录下RSA使用公钥进行加密&#xff0c;公钥进行解密&#xff1a; 背景&#xff1a;由于项目要求前后端进行数据加密传输&#xff0c;具体数据使用aes进行加密&#xff0c;aes密钥使用rsa进行加密&#xff0c;加密后的aes密钥放在请求头和响应头进行传输。这里实现的是前端vue…

Toolify.ai 帮助你发现最好的 AI 网站和 AI 工具

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 人工智能作为一门前沿科技领域&#xff0c;吸引着越来越多的人关注和投入。首先&#xff0c;让我们探讨一下为什么对人工智能感兴趣是值得的&#xff1a; 科技进步与应用&#xff1a;近年来&#xff0c…

javaScript实现客户端直连AWS S3(亚马逊云)文件上传、断点续传、断网重传

写在前面&#xff1a;在做这个调研时我遇到的需求是前端直接对接亚马逊平台实现文件上传功能。上传视频文件通常十几个G、客户工作环境网络较差KB/s&#xff0c;且保证上传是稳定的&#xff0c;支持网络异常断点重试、文件断开支持二次拖入自动重传等。综合考虑使用的Aws S3的分…

C语言:详解操作符(下)

上一篇链接&#xff1a;C语言&#xff1a;详解操作符&#xff08;上&#xff09;摘要&#xff1a; 在上篇文章中&#xff0c;我们已经讲过位操作符等涉及二进制的操作符&#xff0c;这些有助于帮助我们后期理解数据如何在计算机中运算并存储&#xff0c;接下来本篇将更多的讲述…

黑马程序员——移动Web——day01

目录&#xff1a; 平面转换 简介平移定位居中案例-双开门旋转转换原点案例-时钟多重转换缩放案例-播放特效倾斜渐变 线性渐变案例-产品展示径向渐变综合案例 导航-频道渐变按钮轮播图猜你喜欢 1.平面转换 简介 作用&#xff1a;为元素添加动态效果&#xff0c;一般与过渡配…

使用文件读取的open 函数,让你的csv pandas 尾部插入快如闪电

文章目录 简介1. pandas loc 尾部插入方法loc 尾部插入的速度 2. open 方法open方法 处理csv的速度open方法 处理csv代码 简介 笔者在处理稍大型(几十万条)的csv文件时&#xff0c;发现在csv文件中&#xff0c;使用panda的loc方法进行拼接&#xff0c;速度太过于缓慢。 笔者提…