排序【归并排序和计数排序】

news2024/9/22 7:37:06

1.归并排序

1.1 基本思想

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

1.2 递归法:

也就是说,我们在得到有序列之前,要保证俩个子集有序(左区间和右区间),但子集又要分成俩个小子集来使子集有序,小子集又能分出俩个更小的子集,直到子集无法在分,也就是子集仅剩一个元素。

由于我们不能太早的修改原数组的内容,所以我们需要开辟一块新空间(tmp)来暂时存放内容,等内容全部排好序,再将tmp拷贝给原数组。

由于是递归,如果在开辟空间的函数递归,就会一直开辟空间、释放空间,这样会有效率低下的问题,所以我们用一个子函数来完成排序的部分。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void _MergeSort(int* a, int* tmp, int begin, int end);
//归并排序
void MergeSort(int* a, int n)
{
	//开辟的空间
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	//排序主体
	_MergeSort(a, tmp, 0, n-1);

	free(tmp);
	tmp = NULL;
}

void TestMergeSort()
{
	int a[] = { 9,8,6,2,3,4,5,1,7,10 };
	MergeSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}

1.2.1 子函数

void _MergeSort(int* a, int* tmp, int begin, int end);

我们先将数组进行分割,分割点就为中间点(mid),分割出 [ b e g i n , m i d ] [ m i d + 1 , e n d ] [begin, mid][mid+1, end] [begin,mid][mid+1,end],然后当集合只剩下一个元素的时候就跳出递归

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;

	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);
}

1.2.2 排序主体

然后我们就来完成排序的主体部分,因为我们是用区间来分割数组,所以我们需要四个变量来记录俩个区间的开始和结束。
除此之外,我们还需要一个下标变量来记录元素的所在位置,那么我们给下标初始化就不能给0,而是要给相对位置begin

我们判断俩个区间哪个值更小(更大),将较小的值存放到tmp数组里,然后让较小的begin++,直到俩个区间中的任意一个区间没有值(begin == end),就跳出循环,然后将有值区间内的剩余值放到tmp数组,最后将tmp数组的值拷贝给原数组。

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;

	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, 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])
		{
			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, (end - begin + 1) * sizeof(int));
}

这个方法与二叉树的后序遍历很相似,先排序后左区间,在排序右区间,最后在将左右区间结合。

完整代码:

用例:int a1[] = { 9, 8, 6, 2, 11, 3, 4, 5, 1, 10, 7 }; 长度为11

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

void PrintArray(int* a, size_t n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;

	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, 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])
		{
			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, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	_MergeSort(a, tmp, 0, n-1);

	free(tmp);
	tmp = NULL;
}

在这里插入图片描述

1.3 非递归方法:

出发点并不是先排序一个区间再排序另一个区间这样的深度优先遍历(DFS),而是一层一层排序的广度优先遍历(BFS)

类似下图👇:
在这里插入图片描述

1.3.1 初版

由于我们不是递归,所以我们不会用分割,而是用分组,确定每组的元素个数,再进行排序。

我们使用gap来代表每组的个数,然后两组两组的归并,归并一轮后增加gap的数量,直到gap大于大于数组长度,当然也是要将tmp的值拷贝给原数组。

//循环版
void MergeSort2(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	//分组 , gap为每组有多少个元素
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			//左区间
			int begin1 = i, end1 = i + gap - 1;

			//右区间
			int begin2 = i + gap, end2 = i + gap * 2 - 1;

			//下标
			int j = i;
			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);
	tmp = NULL;
}

在这里插入图片描述
可以看到运行成功了,但是,这个排序算法仅仅支持长度为2的次方倍的数组,因为gap是以2的倍数增长,那么当这个数组长度不是2的次方倍的话,就一定会越界访问。

举例:int a2[] = { 9,8,6,2,3,4,5,1,10,7 };
在这里插入图片描述
可以看到报错了

1.3.2 优化

我们先来看看左右区间的下标(原数组的长度是10,最后一个元素的下标为9)
在这里插入图片描述
我们可以看到画红线的就是越界了的
我们可以这样解决:当begin2越界,就代表end2肯定也是越界的,那么这次就不排序,直接退出,如果只有end2越界,那我们就将end2修正成 n − 1 n-1 n1,也就是最后一个元素的下标。
这样可以不需要管end1越界的情况,因为end1越界了,begin2肯定也越界了,就会直接退出。

			//左区间
			int begin1 = i, end1 = i + gap - 1;

			//右区间
			int begin2 = i + gap, end2 = i + gap * 2 - 1;

			if (begin2 > n)
			{
				break;
			}
			if (end2 > n)
			{
				end2 = n - 1;
			}

这样我们就不用管这个数组是不是2的倍数,是不是偶数,因为会将end2修正为n-1,归并并没有规定排序规定两个子集的长度是一样的,所以我们可以把一部分已经排好序但是begin2越界的组留下来,等到最后排序(中途也有可能排序)

完整代码:

用例:int a2[] = { 9,8,6,2,11,3,4,5,1,10,7 }; 长度为11

//循环版
void MergeSort2(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	//分组 , gap为每组有多少个元素
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			//左区间
			int begin1 = i, end1 = i + gap - 1;

			//右区间
			int begin2 = i + gap, end2 = i + gap * 2 - 1;
			//printf("[%d][%d] [%d][%d]  ", begin1, end1, begin2, end2);
			
			//begin2越界,就代表这个区间不存在
			if (begin2 >= n)
			{
				break;
			}
			
			//只有end2越界,修正
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			
			//下标
			int j = i;
			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);
	tmp = NULL;
}

在这里插入图片描述

1.4 特性总结

  1. 归并的缺点就在于需要 O ( N ) O(N) O(N)的空间复杂度,归并排序主要是解决数据在磁盘(固态硬盘)中的外排序问题。
  2. 时间复杂度: O ( N ∗ l o g N ) O(N*logN) O(NlogN)
  3. 空间复杂度: O ( N ) O(N) O(N)
  4. 稳定性:稳定

2. 计数排序

计数排序其实是一种非比较排序,是不需要进行数据之间比较大小的排序。
操作步骤:

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

2.1 操作讲解:

我们既然要统计次数,那么就需要建立一个tmp数组(要将tmp数组里的元素都初始化成0)。

在这里插入图片描述
然后我们遍历一遍a数组,计算a数组下标元素出现的次数,拿a[0]也就是9来举例。
a数组遍历到9的时候,那么tmp数组中下标为9的元素就+1,换成代码就是++tmp[a[i]],将a[i]作为tmp的下标。

在这里插入图片描述
这样依次类推,直到遍历完a数值,然后再遍历一遍tmp数组,将tmp数组中非零值的下标依次拷贝到原数组中。
在这里插入图片描述
但是,我们这里使用的绝对位置,如果这些数是从100开头,然后只排序10个数(101到110),那么我们真正会利用到的也就是一百以后的空间,前100个空间就就浪费了,所以我们要用相对位置。

2.2 相对位置

我们要在上文的遍历a数组开辟tmp前,先遍历一遍a数组,找到其中的最大值和最小值,然后让他们相减再加一,就是我们需要的范围了(int range = max - min + 1;),就拿上文中a数组的0~9来说吧,先遍历一遍,找到min = 0, max = 9,那么我们再根据range的公式得出 range = 10,再用range来开辟tmp数组。

void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];
	
	//通过遍历来获取max和min的值
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i] > max)
		{
			max = a[i];
		}
	}
	
	//计算得出range的值并开空间
	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("calloc fail");
		return;
	}
	//排序……
}

然后再进行上文的操作,但到了回收的时候就要注意了,这时就不能是直接让tmp下标赋值给a数组,而是a[i++] = j + min(i为a数组的下标,j为tmp数组的下标)。

2.3 完整代码

用例:int a1[] = { 9, 8, 6, -2, -11, 3, 4, 5, 1, 10, 7,10,7 ,9, 8, 6, 3, 4, 5, 1, 7, 7, 8, 6, -2, -11, 5, 1 };

void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];

	//通过遍历来获取max和min的值
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i] > max)
		{
			max = a[i];
		}
	}
	//计算得出range的值并开空间
	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 j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i])
		{
			a[j++] = i + min;
			--count[i];
		}
	}

	free(count);
	count = NULL;
}

在这里插入图片描述

可以看到这个计数排序的思想也是可以解决负数的问题。

2.4 特性总结

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

3.总结

本文主要讲解了归并排序和计数,归并排序讲解了递归与非递归的版本,递归版本是采用了二叉树遍历中后序遍历的思想(先遍历左右孩子,再遍历自己),而非递归的重点是对于区间的掌控。

计数排序其实是非比较排序的一种,还有俩个比较出名的非比较排序(基数排序和桶排序),由于逻辑复杂且几乎没有实践意义,在本文就没有讲解,计数排序的重点在于开辟空间的时候要使用相对位置,且回收时是用tmp下标加min来回收。

最后感谢您能阅读完此片文章,如果有任何建议或纠正欢迎在评论区留言,也可以前往我的主页看更多好文哦(点击此处跳转到主页)。
如果您认为这篇文章对您有所收获,点一个小小的赞就是我创作的巨大动力,谢谢!!!

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

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

相关文章

【代理模式】设计模式系列:实现与最佳实践(掌控访问的艺术)

文章目录 Java中的代理模式引言1. 代理模式概念1.1 代理模式定义1.2 代理模式的参与者1.3 代理模式的基本工作原理1.4 代理模式的优点与缺点 2. Java代理模式实现方式2.1 静态代理2.2 动态代理2.2.1 JDK动态代理2.2.2 CGLIB动态代理 2.3 两者之间的区别与选择 3. 使用案例分析3…

Mariadb数据库本机无密码登录的问题解决

Mariadb数据库本机无密码登录的问题解决 安装了mariadb后&#xff0c;发现Mariadb本机无密码才能登录 百度了很多文章&#xff0c;发现很多人是因为root的plugin设置的值不正确导致的&#xff0c;unix_socket可以不需要密码&#xff0c;mysql_native_password 是正常的。 解…

Codeforces Round 965 (Div. 2)

前言 有人在过七夕&#xff0c;我在打 cf &#xff0c;还有某人独自一人在学校机房&#xff0c;凌晨一点骑上共享单车回宿舍欣赏沿途的秋风扫落叶。 Standings&#xff1a;2166 题目链接&#xff1a;Dashboard - Codeforces Round 965 (Div. 2) - Codeforces A. Find K Distin…

未来能源技术

未来能源技术正处于全球焦点的中心&#xff0c;旨在应对气候变化、资源枯竭和能源安全的挑战。未来能源技术的发展方向集中在可再生能源、能源储存技术、智能电网、核聚变以及新材料的应用等多个领域。 1. 可再生能源技术 1.1 太阳能技术 太阳能技术是未来能源发展的核心领域之…

精密Δ-Σ ADC的有效噪声带宽

1 简介 即使对最有经验的模拟设计工程师来说&#xff0c;理解ADC噪声也是一项挑战。Δ-Σ ADC具有量化噪声和热噪声&#xff0c;其变化取决于ADC的分辨率、参考电压和输出数据速率。在系统层面上&#xff0c;噪声分析因附加的信号链组件而变得更加复杂&#xff0c;这些组件中的…

NLP_情感分类_序列模型方案

文章目录 项目背景代码导包读取数据文本预处理举例查看分词器数据集调整进一步剖析&#xff1a;对应Step [{i1}/{len(train_loader)}] 里的train_loader进一步剖析&#xff1a;Step [{i1}/{len(train_loader)}] 里的train_loader&#xff0c;原始的train_df 计算数据集中最长文…

Java 并发(四)—— volatile 和 synchronized

一、volatile 关键字 1.概念 如果我们将一个变量使用 volatile 修饰&#xff0c;这就指示 编译器&#xff0c;这个变量是共享且不稳定的&#xff0c;每次使用它都到主存中进行读取。 2.作用 保证变量对所有线程的可见性。但不能保证数据的原子性。因此不能完全保证线程安全…

STP(生成树)的概述和工作原理

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

PHPStorm 环境配置与应用详解

​ 大家好&#xff0c;我是程序员小羊&#xff01; 前言&#xff1a; PHPStorm 是 JetBrains 出品的一款专业 PHP 集成开发环境&#xff08;IDE&#xff09;&#xff0c;凭借其智能的代码补全、调试功能、深度框架支持和前端开发工具&#xff0c;为用户提供了丰富的功能和工具…

简单的敏感词提示功能

简单的敏感词提示功能 1. 需求 公司现在接到通知&#xff0c;部分接口的部分手动输入字段&#xff0c;需要新增敏感词报红提示&#xff0c;敏感词汇现在应该是7000多个左右&#xff0c;需要我们提供一个敏感词校验接口&#xff0c;如果前端输入敏感词&#xff0c;则前端提示出…

在Unreal Engine中使用C++创建基础角色并添加移动功能

目录 引言 步骤一&#xff1a;创建C类 步骤二&#xff1a;编写C代码 步骤三&#xff1a;设置输入绑定 步骤四&#xff1a;在UE编辑器中测试 结论 引言 Unreal Engine&#xff08;UE&#xff09;以其强大的功能和灵活性在游戏开发界广受好评。本文将指导你如何在UE中通过…

校园外卖平台小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;商家管理&#xff0c;菜品信息管理&#xff0c;菜品分类管理&#xff0c;购买菜品管理&#xff0c;订单信息管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&a…

揭开ChatGPT进化之谜:人工智能如何变得更聪明

近年来&#xff0c;人工智能&#xff08;AI&#xff09;领域取得了显著进展&#xff0c;尤其是在自然语言处理&#xff08;NLP&#xff09;方面。OpenAI的GPT系列模型&#xff0c;如GPT-3和ChatGPT&#xff0c;代表了这一领域的前沿技术。本文将围绕ChatGPT提升的原因、发展趋势…

基于JSP的个性化影片推荐系统

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;JSP 数据库&#xff1a;MySQL 技术&#xff1a;JSP技术 工具&#xff1a;MyEclipse、Tomcat、MySQL 系统展示 首页 管理员功能模块 用户功能模块 …

国外评论家称《黑神话》PC版有性能问题 还有卡顿现象

《黑神话&#xff1a;悟空》即将正式发售&#xff0c;人们对这款游戏的期待值相当高。所以许多粉丝在耐心等待第一批评测报道&#xff0c;然后再购买这款游戏。在8月16日媒体评测解禁之前&#xff0c;有一位国外评论家认为《黑神话&#xff1a;悟空》是一款好游戏&#xff0c;但…

Vue的监视属性watch、computed和watch之间的区别

目录 1. 监视属性2. 监视属性的简写3. computed和watch之间的区别 1. 监视属性 监听对象: 监视属性可以监听普通属性和计算属性调用时间: 当监听的属性发生改变时。handler被调用immediate: true: 是否初始化时让handler调用一下。此时oldVlue为undefineddeep: false: watch默…

美国洛杉矶大带宽服务器的运维与监控

美国洛杉矶的大带宽服务器因其优越的地理位置、高速的网络连接以及充足的带宽资源&#xff0c;在全球范围内享有很高的声誉。为了确保这些服务器的稳定运行和高效服务&#xff0c;运维与监控工作显得尤为重要。以下是一些关于美国洛杉矶大带宽服务器运维与监控方面的科普内容。…

CentOS7 配置 nginx 和 php 方案

配置方案 一、安装软件二、编写配置文件&#xff0c;连接PHP三、引用文件四、测试 鉴于网上教程错综复杂&#xff0c;写下一这篇文章 本教程只需要三步即可 一、安装软件 yum install -y nginx php php-fpm二、编写配置文件&#xff0c;连接PHP 一般情况下在安装完 nginx 后…

css实现太极图

<template><div><!-- 太极图 --><div class"all"><div class"left box"></div><div class"right box"></div><div class"black"><div class"inner_white"><…

16.3 简单神经网络的实现

欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;欢迎订阅相关专栏&#xff1a; 工&#x1f497;重&#x1f497;hao&#x1f497;&#xff1a;野老杂谈 ⭐️ 全网最全IT互联网公司面试宝典&#xff1a;收集整理全网各大IT互联网公司技术、项目、HR面试真题.…