[数据结构基础]排序算法第四弹 -- 归并排序和计数排序

news2024/12/25 9:24:22

目录

一. 归并排序

1.1 归并排序的实现思想

1.2 归并排序的递归实现

1.2.1 归并排序递归实现的思想

1.2.2 归并排序递归实现的代码

1.3 归并排序的非递归实现

1.3.1 归并排序非递归实现的思想

1.3.2 归并排序非递归实现的代码

1.4 归并排序的时间复杂度分析

二. 计数排序

2.1 计数排序的思想

2.2 计数排序函数代码

2.3 计数排序的时间复杂度、空间复杂度及适用情况分析


一. 归并排序

1.1 归并排序的实现思想

归并排序采用分治的思想实现,对于具有n个数据的待排序数组,先将其前半部分和后半部分都排列为有序,然后将前半部分和后半部分视为不同的两个有序序列,将这两个有序序列合并,得到的新的有序序列就是原序列排序后的结果。图1.1展示了归并排序的实现过程。

图1.1 归并排序的实现思想

由图1.1,为了保证前半部分和后半部分有序,在将数组拆分为两部分后继续拆分,直到每组数据中仅有一个数据,单个数据可视为有序序列。完成整体拆分后,即拆到每组只有一个数据,将数据合并为每组两个数据,每组的两个数据有序,之后继续执行合并操作,直到数组中所有数据有序。

1.2 归并排序的递归实现

1.2.1 归并排序递归实现的思想

  • 定义一个主递归排序函数MergeSort,其参数包括待排序数组a和数据个数n,在Mergesort函数中要开辟一块内存空间tmp临时存储部分排好序的数据,还要再调用一个子函数_MergeSort,这个函数的功能是将下标[left, right]之间的数据采用归并排序的方式排好。
  • _MergeSort函数对[left,right]之间的数据排序。取mid=(left+right)/2,应当保证[left,mid]以及[mid+1,right]之间的数据有序,为此,采用递归的方法,对[left,right]之间的数据进行拆分,直到保证每组只有一个数据时才开始排序合并。因此,递归的终止条件为:left<=right。
  • 分组对数据进行合并,直到整个数组中的数据有序。

1.2.2 归并排序递归实现的代码

void _MergeSort(int* a, int* tmp, int left, int right)
{
	//如果区间左值大于等于区间右值,停止拆分
	if (left >= right)
	{
		return;
	}

	int mid = (left + right) / 2;

	_MergeSort(a, tmp, left, mid);  //左区间归并排序
	_MergeSort(a, tmp, mid + 1, right);  //右区间归并排序

	//区间[left,mid] [mid+1,right]中的数据均有序
	//合并两组数据成新的有序序列
	int begin1 = left, end1 = mid;  //左区间的起始下标和结束下标
	int begin2 = mid + 1, end2 = right;  //右区间的起始下标和结束下标
	
	//排升序
	int index = begin1;  //控制tmp的下标

	//先将[left,right]区间的数据按顺序排序存入tmp的[left,right],然后拷贝会原数组a
	while (begin1 <= end1 && begin2 <= end2)
	{
		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++];
	}

	//将tmp中的数据拷贝回a
	for (int i = left; i <= right; ++i)
	{
		a[i] = tmp[i];
	}
}

void MergeSort(int* a, int n)  //递归实现归并排序函数
{
	assert(a);

	//开辟临时空间存储用于临时存储部分排序后的数据
	int* tmp = (int*)malloc(n * sizeof(int)); 
	if (NULL == tmp)
	{
		printf("malloc fail\n");
		exit(-1);
	}

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

	free(tmp);
	tmp = NULL;
}

1.3 归并排序的非递归实现

1.3.1 归并排序非递归实现的思想

归并排序的非递归实现思想与递归类似,都是先将待排序数组中的有数据分组,先将数组中的每个数据单独视为一组,从左侧第一组数据开始,将每两个数据合并为新的组,然后继续按组合并数据,直到完成排序。

与递归不同的是,非递归引入变量gap来控制两组待合并的有序序列首元素的下标之差。如图1.2所示,取begin1和end1分别为左侧有序序列起始下标和结束下标,取begin2和end2位右侧有序序列的起始下标和结束下标,程序中用循环参数i来控制左侧有序序列起始下标,因此:左侧有序序列下标范围[i, i+gap-1],即[begin1,end1],右侧有序序列下标范围是[i+gap,i+2*gap-1],即[begin2,end2]。将左右两边的有序序列合并,使区间[begin1,end2]之间的数据有序。

更新gap的值依次为2、4、8、....,重复执行上段叙述的操作,直到gap<n不成立为止(n为待排序数据的个数)。

图1.2 冒泡排序的非递归实现流程

图1.2展示的数组有n=8个数据,满足n=2^i,因此,begin1、end1、begin2、end2全部没有发生越界,但是,如果待排序数据个数不满足n=2^i,那么end1、begin2、end2则有可能发生越界,begin1一定不会发生越界。越界可分三种情况进行讨论:

  1. end1、begin2、end2均越界。
  2. end1不越界,begin2和end2越界。
  3. end1和begin2不越界,end2越界。

图1.3展示了两种越界情况,当对7个数据进行排序时,gap=1时存在end1不越界,begin2和end2越界的情况,gap=2时存在end1和end2均越界,end2越界的情况。

图1.3 越界情况

对于越界的处理可以分为两种情况讨论:

  • 对于end1、begin2、end2均越界的情况以及end1不越界,begin2和end2越界的情况,区间[begin2,end2]不存在,无需从临时存储数据的区间tmp中拷贝数据回原数组,此时直接break掉本次循环即可。
  • 对于end1和begin2不越界,end2越界的情况,调整end2=n-1,使区间[begin2,end2]不再越界即可。

处理完越界的情况,即可执行两有序序列的合并操作,直到排序完成。

1.3.2 归并排序非递归实现的代码

//归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	assert(a);

	int* tmp = (int*)malloc(n * sizeof(int));  //临时存储排序好的数据
	if (NULL == tmp)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	int gap = 1; //左右两侧的有序序列下标差
	while (gap < n)
	{
		int i = 0;  //循环参数

		for (i = 0; i < n; i += 2 * gap)
		{
			//划分左右两侧区间的起始下标和终止下标
			//左侧:[begin1,end1],右侧:[begin2,end2]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			//当end1越界或begin2越界时,[begin2,end2]不存在,无需从tmp中拷贝数据
			//此时终止循环即可
			if (end1 >= n || begin2 >= n)
			{
				break;
			}

			//仅有end2越界,end1和begin2不越界,[begin2,end2]存在
			//此时修改end2的值,使其不越界即可
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			//将[begin1,end1] [begin2,end2]两有序数据整合为一个有序序列
			int index = begin1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin2] < a[begin1])
				{
					tmp[index++] = a[begin2++];
				}
				else
				{
					tmp[index++] = a[begin1++];
				}
			}

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

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

			//将tmp中区间[begin1,end2]之间的数据拷贝回原数组a的对应区间
			int j = i;
			for (j = i; j <= end2; ++j)
			{
				a[j] = tmp[j];
			}
		}

		gap *= 2;  //更新gap
	}

	free(tmp);
	tmp = NULL;
}

1.4 归并排序的时间复杂度分析

假设待排序的数据个数为N,观察图1.1可得,总共需要被拆分为logN层,拆分完成后逐层进行合并,每层合并需要遍历全部待排序数据,即每层要遍历数据N次,因此,整个合并过程要执行的操作次数可以近似认为是NlogN,拆分过程执行的操作相对于合并过程可以忽略不计。因此,归并排序的时间复杂度为:O(NlogN)

二. 计数排序

2.1 计数排序的思想

计数排序采用的是映射的思想,假设一组待排序数组中数据的最小值为min,最大值为max,开辟一块计数数组空间*count,其中count的空间大小应能存储range=max-min+1个数据,count[0]的值为最小值min出现的次数,count[1]的值为次小值出现的次数,...,count[range]为最大值max出现的次数。根据count中每个数据的值对数据进行排序。

如图2.1所示,对数组a[] = {-2,0,0,0,2,2,3}进行排序,其中min=-2,max=3,因此count应该能容纳range = 3-(-2)+1=6个数据。数组a中,-2对应count下标为0的位置,-2出现一次,因此count[0]=1,-1对应下标为1的位置,-1出现0次,因此count[1]=0。count[6]={1,0,3,0,2,1}。

图2.1 计数排序中的映射关系

2.2 计数排序函数代码

//计数排序函数
void CountSort(int* a, int n)
{
	assert(a);

	int min = a[0];
	int max = a[0];   //数组a中最大值和最小值

	//获取数组中的最大值和最小值
	int i = 0;
	for (i = 1; i < n; ++i)
	{
		if (a[i] > max)
		{
			max = a[i];
		}

		if (a[i] < min)
		{
			min = a[i];
		}
	}

	int range = max - min + 1;  //数据的范围
	int* count = (int*)malloc(range * sizeof(int));  //计数数组
	if (NULL == count)
	{
		perror("malloc");
		exit(-1);
	}
	memset(count, 0, range * sizeof(int));  //计数数组所有元素初始化为0

	for (i = 0; i < n; ++i)
	{
		count[a[i] - min]++;
	}

	//将count中的计数情况拷贝回原数组a
	int index = 0;
	for (i = 0; i < range; ++i)
	{
		while (count[i]--)
		{
			a[index++] = i + min;
		}
	}

	free(count);
	count = NULL;
}

2.3 计数排序的时间复杂度、空间复杂度及适用情况分析

假设要对N个数据进行排序,计数排序首先要遍历一遍待排序数据获取计数数组count,遍历待排序数据的时间复杂度为O(N),生成count后,要再遍历一遍count以获取排序后的序列,count中含有的数据个数为range=max-min+1,遍历count数组的时间复杂度为O(range),因此:计数排序的时间复杂度为O(max(N,range)),空间复杂度为O(range)

对于数据范围range较小,即max-min较小的一组数据,计数排序是八大排序算法中唯一能做到时间复杂度为O(N)的排序算法。但对于range远大于N的一组数据,采用计数排序不仅效率低,且需要消耗大量的内存空间。

综上,得出结论:

  • 计数排序的时间复杂度为O(max(N,range)),空间复杂度为O(range)。
  • 计数排序适用于range较小的数据集,是八大排序算法中唯一有可能达到时间复杂度为O(N)的排序算法。
  • 如果range远大于N,计算排序的性能会比较差。

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

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

相关文章

c++之模板【进阶版】

前言 对于泛型编程&#xff0c;学好模板这节内容是非常有必要的。在前面学习的STL中&#xff0c;由于模板的可重性和扩展性&#xff0c;几乎所有的代码都采用了模板类和模板函数的方式&#xff0c;这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。 模板初阶 …

Hugging face教程-使用速查表-快速入门

Hugging face笔记 course url&#xff1a;https://huggingface.co/course/chapter5/8?fwpt 函数详细情况&#xff1a;https://huggingface.co/docs/transformers/main_classes/pipelines#transformers.TokenClassificationPipeline 基础掌握transformers和datasets&#xf…

软件测试 利器 | AppCrawler 自动遍历测试工具实践(一)

本文为霍格沃兹测试学院学院学员课程学习笔记&#xff0c;系统学习交流文末加群。 AppCrawler 是由霍格沃兹测试学院校长思寒开源的一个项目,通过名字我们大概也能猜出个方向&#xff0c;Crawler 是爬虫的意思&#xff0c;App 的爬虫&#xff0c;遍历 App &#xff1a; 官方 G…

linux性能优化-中断

一、概念 中断其实是一种异步的事件处理机制&#xff0c;可以提高系统的并发处理能力。Linux将中断处理过程分成了两个阶段&#xff1a;上半部和下半部 &#xff08;1&#xff09;上半部用来快速处理中断&#xff0c;它在中断禁止模式下运行&#xff0c;主要处理跟硬件紧密相关…

云计算是什么

&#x1f4d2;博客主页&#xff1a; 微笑的段嘉许博客主页 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐留言&#x1f4dd; &#x1f4cc;本文由微笑的段嘉许原创&#xff01; &#x1f4c6;51CTO首发时间&#xff1a;&#x1f334;2023年2月1日&#x1f334; ✉…

gcc 简介

一、gcc简介gcc与g&#xff0c;当程序中出现using namespace std等带有c特性的语句时&#xff0c;如果用gcc编译时&#xff0c;必须显式地指明这个程序要用c编译库编译&#xff0c;而g可以直接编译。二、gcc支持的文件.c&#xff0c;c语言的源程序.C, c的源程序.cc&#xff0c;…

数据结构——堆的介绍以及应用

前言&#xff1a;对于数据结构而言&#xff0c;大多存在着对应的物理结构和逻辑结构&#xff0c;而我们一开始介绍的顺序表&#xff0c;链表&#xff0c;栈&#xff0c;队列等的物理结构和逻辑结构还是比较类似的。今天要介绍的堆则有所不同&#xff0c;其物理结构是数组&#…

JS前端基于canvas给图片添加水印,并下载带有水印的图片

基于canvas给图片添加水印实现效果图图片添加水印的步骤1.获取图片路径&#xff0c;将图片转换为canvas2.canvas画布上绘制文字水印3.水印绘制完成后&#xff0c;将canvas转换为图片格式4.水印绘制完成后&#xff0c;将canvas下载为图片完整代码总结1、在utils.js 封装添加水印…

POE交换机全方位解读(中)

POE供电距离到底怎么算 只针对符合IEEE802.3af/at 标准PoE设备 ① 网线对供电距离的影响 首先我们先来看下表IEEE802.af和IEEE802.3at标准中对Cat5e网线要求&#xff1a; 说明&#xff1a;Type 1 value和Type 2 value 分别指IEEE802.3af和IEEE802.3at的要求。 从表中可以看出&a…

PCB电路板单面板和双面板的区别和共同点

PCB电路板可以分为单面板、双面板和多面板&#xff0c;我们常用的主要是单面板和双面板&#xff0c;那么单面板和双面板有哪些区别呢&#xff1f;在了解二者区别前&#xff0c;沐渥小编先给大家介绍一下什么是单面板和双面板。 单面板是指单面的线路板&#xff0c;元器件在一面…

如何实现报表集成?(四)——权限集成

在上一篇&#xff0c;我们介绍了报表工具的资源集成&#xff0c;基本知道了报表工具链接、模块、页面和移动端如何实现集成。 这一篇&#xff0c;我们看下如何做权限集成。使用第三方系统的资源权限验证 实际上往往存在多个系统需要统一权限认证&#xff0c;用户要求将某个系统…

PixelLib图像分割

文章目录前言一、PixelLib依赖安装二、实例模型训练前言 图像分割就是把图像分成若干个特定的、具有独特性质的区域并提出感兴趣目标的技术和过程。它是由图像处理到图像分析的关键步骤。 传统的图像分割方法主要分以下几类&#xff1a;基于阈值的分割方法、基于区域的分割方…

Mybatis核心原理梳理

文章目录Mybatis的简单使用Mybatis组件名词介绍Mybatis主要工作流程Mybatis如何控制事务Mybatis中事务的生命周期一二级缓存分别如何生效一二级缓存分别如何失效一级缓存的实体可能会被修改Mybatis中的已经存在PooledDataSource连接池为啥还选择Durid等为啥连接close之后被没有…

如何获取 WWDC 视频对应的官方源代码?

零 概览 每年的 WWDC&#xff08;The Apple Worldwide Developers Conference&#xff09; 是 Apple 开发者的盛大节日&#xff0c;我们可以从 WWDC 海量官方视频中学到大量的知识。 不过&#xff0c;有些视频仅包含一些“惨不忍睹”&#xff08;由于网络质量差等原因&#…

【C++】C++ 入门(二)(引用)

目录 一、前言 二、引用 1、引用的概念 2、引用特性 3、使用场景 3.1、做参数 3.2、做返回值 4、传值、传引用效率比较 值和引用作为参数的性能比较 值和引用作为返回值类型的性能比较 5、常引用 6、引用和指针的区别 一、前言 上一篇文章我们讲解了 C 的命名空间…

IDEA快速生成实体类(加注释)

步骤&#xff1a; 1、点击右侧的datesource图标&#xff0c;要是没有该图标&#xff0c;请去自行百度 2、点击 号 3、选择 datasource 4、选择 mysql 1、填写一个连接名&#xff0c;随便填什么都行 2、不用选择&#xff0c;默认就行 3、填写数据库连接的 IP地址&#xff0c;比…

Android 时间工具类

最近总结了一下时间相关的用法&#xff0c;如下。 1、日期转换为字符串 默认"yyyy-MM-dd HH:mm:ss" 2、任意类型日期字符串转时间 3、获取当前对应格式的日期 4、获取当前对应格式的日期 默认"yyyyMMddHHmmssSSS" 5、计算该天是星期几 6、获取星期几…

XSS - 进阶篇(蓝莲花的基本使用)

数据来源 本文仅用于信息安全的学习&#xff0c;请遵守相关法律法规&#xff0c;严禁用于非法途径。若观众因此作出任何危害网络安全的行为&#xff0c;后果自负&#xff0c;与本人无关。 xss漏洞接收平台-蓝莲花&#xff1a; 1&#xff09;下载并安装Phpstudy&#xff08;安…

分享157个ASP源码,总有一款适合您

ASP源码 分享157个ASP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 157个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/1_IF9pFQX4NM-kmJyIAGBQQ?pwdcb55 提取码&#x…

RBAC简介

RBAC BAC基于角色的访问控制&#xff0c;RBAC认为权限授权的过程可以抽象地概括为&#xff1a;Who是否可以对What进行How的访问操作 RBAC简介 基于角色的权限访问控制模型 在RBAC模型里面&#xff0c;有3个基础组成部分&#xff0c;分别是&#xff1a;用户、角色和权限。RB…