排序篇:归并排序的递归,非递归以及计数排序的实现(C语言)

news2024/9/28 19:21:46

目录

一:归并排序

(1)归并排序的基本思想

(2)递归版本

①实现思路

②合并

③递归实现有序

④最终代码

(3)非递归版本

①实现思路

②控制分组

③最终代码

(4)时间,空间复杂度分析

(5)小结

二:计数排序

(1)计数排序的基本思想

(2)实现思路

(3)图解加最终代码

(4)时间,空间复杂度分析

(5)小结


一:归并排序

(1)归并排序的基本思想

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


(2)递归版本

①实现思路

【1】把待排序数组从中间分成两个子数组,直到无法分割(即每个子数组只有一个元素)。

【2】对每个子数组进行归并排序,即递归调用归并排序函数。

【3】合并两个有序子数组,得到一个有序的数组(这里需要辅助数组来实现),然后把这个有序数组拷贝到原数组中。

【4】重复步骤3,直到所有的子数组合并成一个有序的数组,排序完成。


②合并

合并两个有序数组需要辅助数组tmp,大致思路就是遍历两个区间,拿出两个数组中较小值放在tmp中,合并成有序后再拷贝回原数组

图解:

这里循环已经结束但我们需要把没结束的一方拷贝到tmp中

    //把没结束的一方拷贝到tmp中
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

合并的代码:

//这个时候左右已经有序,合并
	int begin1 = left;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = right;
	int i = left;

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

	//把没结束的一方拷贝到tmp中
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

	//拷贝回去
	//注意这里合并了的有序区间为[left,right]
	//别的区间不一定有序,拷贝时要注意
	for (int j = left; j <= right; j++)
	{
		a[j] = tmp[j];
	}


③递归实现有序

图解(以左区间为例):


④最终代码

void _MergeSort(int* a, int left,int right,int* tmp)
{
	//只有一个元素(可看成有序)或者区域不存在,返回
	if (left >= right)
	{
		return;
	}
	int mid = left + (right - left) / 2;
	//先递归排序左区间,后递归排序右区间
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	//这个时候左右已经有序,合并
	int begin1 = left;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = right;
	int i = left;

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

	//把没结束的一方拷贝到tmp中
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

	//拷贝回去
	//注意这里合并了的有序区间为[left,right]
	//别的区间不一定有序,拷贝时要注意
	for (int j = left; j <= right; j++)
	{
		a[j] = tmp[j];
	}
}
//归并排序
void MergeSort(int* a, int n)
{
	//临时数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	
	//调用子函数进行排序
	_MergeSort(a, 0,n-1,tmp);

	//销毁
	free(tmp);
	//这里置不置空没影响
	tmp = NULL;
}

(3)非递归版本

①实现思路

【1】首先将待排序数组中的每一个元素看成是一个大小为1的有序区间

【2】然后将相邻的两个区间(保证每次合成的都是相邻区间,依靠间距gap来控制)合并成一个更大的有序区间,这可以通过归并排序中的合并操作来实现。

【3】重复步骤2,每次将相邻的两个有序子数组合并成更大的有序子数组,直到得到一个完整的有序数组。

【4】最终得到的有序数组就是排序结果。


②控制分组

关于间距gap对循环的控制:

gap=1,范围为1(gap)的区间是有序的,合并相邻两个区间,拷贝。

gap=gap*2=2,范围为2(gap)的区间是有序的,合并相邻两个区间,拷贝。

gap=gap*2=4,范围为4(gap)的区间是有序的,合并相邻两个区间,拷贝。

………………

一直到gap>=n(这个时候数组前n个数一定有序,n是数组元素个数,结束循环)


代码:

    int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + gap * 2 - 1;
			int index = i;

			while (begin1 <= end1 && begin2 <= end2)
			{
				//begin1小,放a[begin1]
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
			//排序一组拷贝一组
			for (int j = i; j <= end2; j++)
			{
				a[j] = tmp[j];
			}
		}
		gap *= 2;
	}

图解:

我们可以看到像前面那样进行分组是有很大可能越界的,那我们应该怎么做呢?

 

合并拷贝前加上区间判断和修正后,排序不会越界了


③最终代码

//归并非递归
void MergeSortNonR(int* a, int n)
{
	//临时数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc error\n");
		exit(-1);
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + gap * 2 - 1;
			int index = i;

			//end1,begin2,end2都有可能越界
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			while (begin1 <= end1 && begin2 <= end2)
			{
				//begin1小,放a[begin1]
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
			//排序一组拷贝一组
			for (int j = i; j <= end2; j++)
			{
				a[j] = tmp[j];
			}
		}
		gap *= 2;
	}

	//释放
	free(tmp);
	tmp = NULL;
}

(4)时间,空间复杂度分析

空间复杂度:

开辟了空间大小和原数组相同的辅助数组,故空间复杂度为O(N)

时间复杂度:

【1】在归并排序的每一次合并操作中,需要将两个有序数组合并成一个有序数组,这个过程需要比较两个有序数组中所有元素,因此时间复杂度为O(N)

【2】在归并排序中,每次将数组划分成两个长度大致相等的子数组,因此可以得到一个完全二叉树,其深度大约为logN。每层的合并操作的时间复杂度为O(N),因此整个算法的时间复杂度为O(N*logN)


(5)小结

归并排序的效率还不错,但是有O(N)的空间复杂度,更多是应用在解决磁盘中的外排序问题。

另外控制边界的方法并不止上面一种

除了右区间不在数组中(左右都越界)直接跳出(这个时候没有对tmp进行操作,对应位置为随机值)

我们也可以把右区间人为修改为不存在(begin>end),这种情况下即使不需要合并也会拷贝到tmp中,我们就可以一次大循环结束再进行拷贝(拷贝一层),但是这样不是很好理解

代码:

//归并非递归
void MergeSortNonR1(int* a, int n)
{
	//临时数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc error\n");
		exit(-1);
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2*gap)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + gap * 2 - 1;
			int index = i;

			//修正,让左区间不越界
			if (end1 >= n)
			{
				end1 = n - 1;
			}
			//修正,让右区间不存在
			if (begin2 >= n)
			{
				//begin2 > end2,区间不存在
				begin2 = n ;
				end2 = n - 1;
			}
			//修正,让右区间不越界
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			while (begin1 <= end1 && begin2 <= end2)
			{
				//begin1小,放a[begin1]
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}

		}
		//一层按组排序完,拷贝
		for (int j = 0; j < n; j++)
		{
			a[j] = tmp[j];
		}
		gap *= 2;
	}
	
	//释放
	free(tmp);
	tmp = NULL;
}


二:计数排序

(1)计数排序的基本思想

对于给定的输入序列中的每一个元素x,确定出小于x的元素个数

这样就可以直接把x放到以小于x的元素个数为下标的输出序列的对应位置上

(这里其实是相对位置的概念,比如数组中最小值为0,它对应下标0位置,最小值为1000,也是对应下标0位置)


(2)实现思路

【1】遍历一遍,找出最大值和最小值

【2】依据最大值和最小值的差值来开辟辅助数组tmp

【3】计数,记录数组元素出现次数

【4】遍历tmp数组,进行拷贝


(3)图解加最终代码

图解:


最终代码:

//计数排序
void CountSort(int* a, int n)
{
	//找出最大和最小
	int max = a[0];
	int min = a[0];
	int i = 0;
	for (int i = 1; i < n; i++)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if (a[i] < min)
		{
			min = a[i];
		}
	}

	//开空间加初始化
	int* tmp = (int*)malloc(sizeof(int) * (max - min + 1));
	if (tmp == NULL)
	{
		printf("malloc error\n");
		exit(-1);
	}
	//必须初始化为0
	memset(tmp, 0, sizeof(int) * (max - min + 1));

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

	//拷贝
	int j = 0;
	for (i = 0; i < max - min + 1; i++)
	{
		while (tmp[i]--)
		{
			a[j++] = i + min;
		}
	}
}


(4)时间,空间复杂度分析

空间复杂度:

开辟了空间大小为range(max-min+1)的辅助数组,故空间复杂度为O(range)

空间复杂度:

遍历a找最大和最小为O(N)

遍历a计数为O(N)

遍历range为O(range)

时间复杂度为O(max(N,range))


(5)小结

计数排序是一种非比较的排序,它不需要进行数据间的比较。

算法设计上非常巧妙,时间复杂度最快可以达到O(N),但是有一定的局限性

比如正负数同时存在或者数据大小浮动很大(1,2,3,1000000)的情况,可能导致空间的大量浪费,效率也会有所下降

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

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

相关文章

asp.net+sqlserver体育器材租赁借还系统

本器材借还系统分为管理员和用户两部分&#xff0c;具体功能如下 管理员部分功能 1.管理员管理&#xff0c;管理系统内所有的管理员信息 2.器材信息管理&#xff0c;对器材的基本信息进行管理&#xff0c;方便用户的租借 3.申请审核管理&#xff0c;当用户申请了器材的使用只&a…

跨境卖家必看系列:沃尔玛美国站入驻教程

沃尔玛自从2020年开始重点发展线上商店以来&#xff0c;销售额一直都很可观。前段时间&#xff0c;沃尔玛美国电商还开了个全球招商战略发布会。所以今天龙哥就根据会议官方发布的步骤&#xff0c;给大家总结一下想要入驻沃尔玛美国站的话需要怎么操作。 沃尔玛的入驻渠道 1.…

35-40的技术人员为什么会被“不友好”,请你们自身反思-拒做职场的“嗯嗯”怪

35-40真的是IT人员的一道坎吗&#xff1f; IT技术做不到35-40&#xff0c;可是我身边有大量35-40事业发达、职业发展更好的朋友。同时&#xff0c;我身边也有大量35-40被“毕业”的人更多。 本人经过7年来先后带队过3个大型研发团队&#xff0c;最少的也有60-70号人。最多的达到…

谈谈MySQL的InnoDB存储引擎

大家好&#xff0c;我是易安&#xff01; 今天我们谈一谈MySQL中InnoDB存储引擎。InnoDB存储引擎作为我们最常用到的存储引擎之一&#xff0c;充分熟悉它的的实现和运行原理&#xff0c;有助于我们更好地创建和维护数据库表。 InnoDB体系架构 InnoDB主要包括了内存池、后台线程…

【深入浅出 Yarn 架构与实现】6-3 NodeManager 分布式缓存

不要跳过这部分知识&#xff0c;对了解 NodeManager 本地目录结构&#xff0c;和熟悉 Container 启动流程有帮助。 一、分布式缓存介绍 主要作用就是将用户应用程序执行时&#xff0c;所需的外部文件资源下载缓存到各个节点。 YARN 分布式缓存工作流程如下: 客户端将应用程序…

电脑——如何配置一台电脑

一、需要买那些东西 主板&#xff1a;显卡、电源、CPU、内存条、硬盘、显示器、鼠标、键盘、音响 怎么开始&#xff1f; 知乎黑虾 首先确定CPU型号再选择与该CPU兼容的主板&#xff08;不同的CPU对应的主板插槽类型是不同的&#xff09; 如何选择CPU型号&#xff1a; 主要…

HttpClient入门介绍

介绍 介绍 HttpClient 是Apache Jakarta Common 下的子项目&#xff0c;可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包&#xff0c;并且它支持 HTTP 协议最新的版本和建议。 作用&#xff1a; 发送HTTP请求 接收响应数据 应用场景&#xff1a; …

Unreal Engine11:触发器和计时器的使用

写在前面 主要是介绍一下触发器和计时器的使用&#xff1b; 一、在Actor中使用触发器 1. 新建一个C类 创建的C类也是放在Source文件夹中的Public和Private文件夹中&#xff1b;选择Actor作为继承的父类&#xff1b;头文件包括一个触发器和两个静态网格&#xff0c;它们共同…

基于SpringBoot,vue的家政服务平台的设计与实现

背景 以往的家政服务管理平台的管理&#xff0c;一般都是纸质文件来管理家政服务信息&#xff0c;传统的管理方式已经无法满足现代人们的需求&#xff1b;使用家政服务管理平台, 首先可以大幅提高家政服务信息检索&#xff0c;只需输入家政服务相关信息就能在数秒内反馈想要的…

真题详解(哈希冲突)-软件设计(七十)

真题详解(3FN)-软件设计&#xff08;六十九)https://blog.csdn.net/ke1ying/article/details/130548812 在以阶段划分的编译器&#xff0c;____阶段的主要作用是分析构成程序的字符及由字符构造规则构成的符号是否复合程序语言的规定。 词法分析 B.语法分析 C.语义分析 D.代码…

Linux 文件系统原理 / 虚拟文件系统VFS

Linux 文件系统原理 / 虚拟文件系统VFS 虚拟文件系统 VFSVFS 定义VFS 的对象演绎超级块 super_block索引节点 inode目录项 dentry文件 file 打开文件流程参考文献 虚拟文件系统 VFS VFS 定义 VFS是一个抽象层&#xff0c;其向上提供了统一的文件访问接口&#xff0c;而向下则…

深度学习笔记之卷积神经网络(二)图像卷积操作与卷积神经网络

深度学习笔记之卷积神经网络——图像卷积操作与卷积神经网络 引言回顾&#xff1a;图像卷积操作补充&#xff1a;卷积核不是卷积函数 卷积神经网络卷积如何实现特征描述/提取卷积神经网络中的卷积核的反向传播过程场景构建与前馈计算卷积层关于卷积核的反向传播过程卷积层关于输…

《花雕学AI》新版必应 Bing 登场:轻松注册,一站式搜索、聊天与绘画应有尽有

引言&#xff1a; 你是否曾经在网上搜索信息时感到困惑或沮丧&#xff1f;你是否曾经想要在网上创造一些有趣或有用的内容&#xff0c;却不知道从何开始&#xff1f;你是否曾经想要用文字描述一个图像&#xff0c;却无法找到合适的图片&#xff1f;如果你的答案是肯定的&#x…

ChatGPT将抢占谁的工作,未来如何应对

“AI人工智能领域里程碑式应用” ChatGPT影响力已经越来越大&#xff0c;激起大家强烈好奇心的同时&#xff0c;也让一些人发出了“感觉自己快要失业了”的焦虑&#xff0c;今天先说一下哪些人的工作会受到 ChatGPT等AI人工智能影响 从工业时代到数字时代这100多年的发展历程来…

【华为机试】——HJ4 字符串分隔

【华为机试】——HJ5 进制转换&#x1f60e; 前言&#x1f64c;HJ4 字符串分隔方法一&#xff1a;巧用scanf的输入格式方法二&#xff1a;循环分解思想 总结撒花&#x1f49e; &#x1f60e;博客昵称&#xff1a;博客小梦 &#x1f60a;最喜欢的座右铭&#xff1a;全神贯注的上…

泛型的特点和深浅拷贝的区别以及不相等对象的hashcode值的问题

永远都不为自己选择的道路而后悔&#xff0c;人生如同坐火车&#xff0c;风景再美也会后退&#xff0c;流逝的时间和邂逅的人终会渐行渐远&#xff0c;前行的始终是自己 泛型常用特点 泛型是JavaSE1.5之后的特性&#xff0c;《Java核心技术》中对泛型的定义是&#xff1a; “…

数据血缘分析工具SQLFLow自动画出数据库的 ER 模型

马哈鱼数据血缘分析器通过分析你所提供的 SQL 脚本&#xff0c;或者连接到数据库&#xff0c;可以自动画出数据库的 ER 模型&#xff0c;可视化表和字段的关系&#xff0c;帮助你迅速了解数据库的设计模型&#xff0c;进行高效的团队沟通。 马哈鱼通过两种途径来为你自动可视化…

MySQL中使用delete_at(时间戳)作为逻辑删除标记时如何使用MyBatis-Plus逻辑删除组件插入时间戳,以及如何解决自动填充失效的问题

背景 MySQL中使用delete_at&#xff08;时间戳&#xff09;作为逻辑删除标记 在业务中&#xff0c;使用逻辑删除是普遍做法&#xff0c;通常会使用一个名为deleted&#xff08;0/1&#xff09;的字段表示删除状态。 但是如果遇到有唯一约束&#xff0c;且可能反复删除和重新…

chatgpt如何引入本地知识?我们来看下emnlp 2022如何将本地图谱知识引入到任务型对话系统中

一、概述 title&#xff1a;Injecting Domain Knowledge in Language Models for Task-Oriented Dialogue Systems 论文地址&#xff1a;https://arxiv.org/abs/2212.08120 代码&#xff1a;GitHub - amazon-science/domain-knowledge-injection 1.1 Motivation 如何在PLM…

SIFT描述子实现

参考&#xff1a;SIFT图像匹配原理及python实现&#xff08;源码实现及基于opencv实现&#xff09; #include <iostream> #include <opencv2/opencv.hpp> #define _USE_MATH_DEFINES #include <math.h> #include <numeric>float mod_float(float x, f…