排序(一)——插入排序 希尔排序

news2024/12/25 23:57:54

1.直接插入排序

直接插入排序是一种简单的插入排序,它的基本思想是:

把待排序的数据按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的数据都插入完位置,就得到了一个新的有序序列。

我们可以看到他的前提是要有一个有序序列,然后插入一个数据到这个有序序列中使其仍然有序,怎么去找一个有序序列呢?我们可以认为,只有一个数据的时候他就是有序的。就比如下面的一个数组:

他用直接排序的思路是怎么排的呢?

首先我们要将第一个数据 8 看成一个有序序列,这时 [0 ,0] 这个区间就是有序的,这时候我们在将第二个数据 5 插入到有序序列中,我们要保证  [0,1]  还是一个有序序列,我们先假设新插入的数据就在最后,这时候他要和前面的数据进行大小比较,5<8 ,所以 5 和 8 要调换位置,这时候5 就到了有序序列的头部,前面已经没有要比较的数据了,这时候 5 就插入完成了 。而新序列 [0 ,1] 这个区间 又是一个新的有序序列。

 这时候再插入 [0,1] 后面的 0 插入到有序序列中, 0首先与8比较,然后0和8调换位置,然后0再与5相比较,0再与5调换位置

这时候 [0,2]这个区间又是一个有序序列了,这时候我们就能写出直接插入排序的单趟排序的代码了。 首先数组中有一个有序区间是 [0 ,end] , 然后再把 a [end+1]插入到有序序列中。

	//[0,end]有序,将a[end+1]插入
	while (end>=0 && a[end+1] < a[end])
	{
		Swap(&a[end], &a[end + 1]);
		end--;
	}

 单趟排序整完了,我们就只要控制 end 就完成整个排序了,而有序序列最开始就只有一个数据,也就是[0,0]这个区间,假如数据个数为n的话,最后一趟排序就是最后一个数据插入到 [0,n-2] 这个有序序列中,所以 end 的最大值就是n-2。再用一层循环来控制end就完成直接插入排序了。

//交换函数
void Swap(int* px, int* py)
{
	int tmp = *px;
	*px = *py;
	*py = tmp;
}


//直接插入排序
void _Insert(int* a,int n)
{
	assert(a);

	int i = 0;
	for (i = 0; i <= n - 2; i++)
	{
		int end = i;
		//[0,end]有序,将a[end+1]插入
		while (end >= 0 && a[end + 1] < a[end])
		{
			Swap(&a[end], &a[end + 1]);
			end--;
		}
	}
}

直接插入排序有什么特点呢?

首先就是直接插入排序对于本来就有序或者接近有序的数据会很有优势,这时候就相当于遍历了一遍数组,基本没有交换,效率很高。

但是直接插入排序对于逆序的数据会很吃力,以为每次插入一个新数据,都要从最后的位置逐个替换到数组首位,时间复杂度为O(N^2)

2.希尔排序

希尔排序是对直接插入排序的一种优化。因为直接插入排序对于逆序的数据排序的时间复杂度很高,所以希尔排序的逻辑是先进行预排序,是数据接近有序,然后再进行直接排序。

希尔排序的第一个步骤就是预排序,把间隔为 gap 的数据分为一组,进行插入排序。

那上面的数组来举例,假设 gap=3 时,把数组中间隔为gap的数据分为一组,然后对他们进行直接插入排序。比如上面的 0 ,1 ,4 ,10是一组,首先 0 看成一个有序序列,把1插入进去,然后再把 4 插入进去 ,最后再把10 插入进去。预排序的逻辑就是这样,跟直接插入排序差不多,直接插入排序的gap相对于1,这里只是将间隔为gap的数据看成一组。

这时候要怎么来控制 end 呢,[0,end]有序,end的最小值就是0,把a[end+gap]插入到有序序列中,那么我们的 end 的最大值就是n-1-gap,每次交换完之后end更新为前一个数据的位置end-gap,那么单趟排序的实现就是下面这样的:

	while (end>=0&&a[end + gap] < a[end])
	{
		Swap(&a[end], &a[end + gap]);
		end -= gap;
	}
	 

我们可测试一下上面的数据当gap为3时单趟排序的结果对不对。

我们画图可以得出的结果是:

每一组间隔为gap的数据都是有序的

//希尔排序
void ShellSort(int* a, int n)
{
	assert(a);
	
	int gap = 3;
	int i = 0;
	for (i = 0; i <= n - 1 - gap; i++)
	{
		int end = i;
		//[0,end]有序,插入a[end+gap]
		while (end >= 0 && a[end + gap] < a[end])
		{
			Swap(&a[end], &a[end + gap]);
			end -= gap;
		}
	}
}

单趟排序搞定之后剩下的就很简单了,希尔排序除了预排序的思想,其次的就是 gap 的控制了,为什么要有 gap 为间隔来预排序? 试想,当原数组的数据是接近逆序的时候,他的大的数据在前面,而小的数据在后面,如果采用直接排序的话就需要一个位置一个位置交换,十分的耗时,当我们用预排序的话,他不是一次只移动一个位置,而是一次移动gap个位置,这样一来能让小的数据更快地来到数组的前面,大的数据也能更快的调换到后面,使得原本逆序的数组变得更接近有序,更有利于插入排序。同时,希尔排序一般是用于排序数据量很大的时候,这时候也能让更大的数据更快的到后面,小的数据更快的到前面,这样优化了直接插入排序。

但是gap该如何确定呢?当数据量大的时候gap相应地就要大,而当数据量小的时候gap相应地就要小,所以gap是与数据个数n有关的。前面我们说过当gap为 1 的时候,希尔排序就是直接插入排序。

思考一个问题,希尔排序只能进行一次预排序吗? 我们可以用多次预排序来使得原数组接近有序,但是每一次的gap都要改变,而最后一趟预排序的 gap 一定要为 1 ,确保最后一趟是直接插入排序。我们可以用数据量  N / 2 来初始化第一趟预排序的gap,然后每一趟预排序的gap都是上一趟的 gap/2 ,这样就能确保最后一趟的gap为1,也可以用 N/3+1来表示第一趟预排序gap,每次的gap是上一次的gap/3+1,这样也能确保gap最后一次为1。多次排序的目的就是为了使得数据更接近有序,两趟相邻预排序的gap不能太过接近,这样的话预排序次数过多,时间复杂度过高,而两趟相邻预排序的gap也不能差距过大,差距太大的话预排序的程度不够,无法使数据接近有序,到最后gap为1时的时间消耗大。 上面我们举例的两种就是目前程序员常用的两种gap的值的定义。

这样一来,我们只要控制好gap就能完成希尔排序了,我们知道最后一趟直接排序的gap为1,如何控制循环跳出条件呢?我们可以在循环内部预排序完了之后用一个判断和break来跳出循环if(gap==1) braek,也可以在循环条件中判断 gap>1 进入循环,然后再预排序之间修改gap,这种情况要把gap初始化为n。

//希尔排序
void ShellSort(int* a, int n)
{
	assert(a);
	
	int gap =n;

	while (gap > 1)
	{
		gap= gap / 3 + 1;
		int i = 0;
		for (i = 0; i <= n - 1 - gap; i++)
		{
			int end = i;
			//[0,end]有序,插入a[end+gap]
			while (end >= 0 && a[end + gap] < a[end])
			{
				Swap(&a[end], &a[end + gap]);
				end -= gap;
			}
		}
	}
}

我们可以来测试一下直接插入排序和希尔排序的效率,看一下希尔排序是否比直接插入排序快。

void test()
{
	int a[] = { 0, 5, 8, 1, 3, 2, 4, 6, 7, 10, 9 };
	int b[] = { 0, 5, 8, 1, 3, 2, 4, 6, 7, 10, 9 };
	int n = sizeof(a) / sizeof(a[0]);

	int begin1 = clock();
	_InsertSort(a, n);
	int end1 = clock();
	printf("_InsertSort:%d\n",end1-begin1);


	int begin2 = clock();
	ShellSort(b, n);
	int end2 = clock();
	printf("ShellSort:%d\n",end2-begin2);
}

clock是一个系统时钟,他能返回程序执行到这行代码的时间,单位是毫秒,当我们用  end-begin就能表示出一个算法所耗的时间。

当数据量很小的时候,因为计算机处理速度很快,几乎就是一瞬间就完成了,看不出差距。

当吧两个数组都换成逆序的数组时,

还是看不出差距。

当我们用一万个随机数据排序的时候。

	int N = 10000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	assert(a1);
	int* a2 = (int*)malloc(sizeof(int) * N);
	assert(a2);
	srand((unsigned int)time(NULL));

	for (int i = 0; i < N; i++)
	{
		int data = rand();
		a1[i] = data;
		a2[i] = data;
	}



	int begin1 = clock();
	_InsertSort(a1, N);
	int end1 = clock();
	printf("_InsertSort:%d\n",end1-begin1);


	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();
	printf("ShellSort:%d\n",end2-begin2);

	free(a1);
	free(a2);

这时候他们的差距就体现出来了,几乎就不在一个量级了。

如果排序一百万了数据呢?(OS:跑了半天没跑出来),换成十万个数据试一下

十万个数据的时候插入排序的时间就很长了。我们还可以加入之前实现的堆排序来对比一下,堆排序这是之前我们所学习的效率最快的排序算法了。

//向下调整 大堆
void AdjustDown(int* a, int begin, int end)
{
	int parent = begin;
	while (parent<end)
	{
		int maxchild = 2 * parent + 1;
		if (maxchild > end)
			break;
		if (maxchild + 1 <= end)
		{
			if (a[maxchild] < a[maxchild + 1])
			{
				maxchild += 1;
			}
		}
		if (a[maxchild] > a[parent])
		{
			Swap(&a[maxchild], &a[parent]);
			parent = maxchild;
		}
		else
		{
			break;
		}
	}
}


//删除堆顶元素
void HeapPop(int* a, int size)
{
	assert(a);

	Swap(&a[0], &a[size - 1]);
	size--;
	AdjustDown(a,0, size-1);
}


//建堆
void CreatHeap(int* a, int size)
{
	assert(a);
	int end = (size - 1 - 1) / 2;
	while (end>=0)
	{
		AdjustDown(a, end, size-1);
		end--;
	}
}

//堆排序
void HeapSort(int* a, int size)
{
	assert(a);

	CreatHeap(a, size);
	while (size)
	{
		HeapPop(a, size);
		size--;
	}
}

我们可以发现,在数据量为100000时,希尔排序已经几乎和堆排序一个等级了,

然后我们测试一百万个随机数排序时堆排序和希尔排序的效率

数据量为一千万时

虽然希尔排序还是比不上堆排序,但是相对于直接插入排序他已经提升的十分大了。

希尔排序的时间复杂度是不好算的,因为每一次预排序都会对下一次预排序产生影响,而现在大多数人认可的希尔排序的时间复杂度就是 O(N^1.3),效率略差于NlogN。

这里我们为什么直接创建数组而是要malloc数组呢?因为正常创建的数组是存储在栈区的,而栈区的空间不是很大,只有几兆的大小,而堆区通常有一两个G,所以大数据存在堆区,而栈区有可能存不下。

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

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

相关文章

部署ELFK+zookeeper+kafka架构

目录 前言 一、环境部署 二、部署ELFK 1、ELFK ElasticSearch 集群部署 1.1 配置本地hosts文件 1.2 安装 elasticsearch-rpm 包并加载系统服务 1.3 修改 elasticsearch 主配置文件 1.4 创建数据存放路径并授权 1.5 启动elasticsearch是否成功开启 1.6 查看节点信息 …

Spring(24) Json序列化的三种方式(Jackson、FastJSON、Gson)史上最全!

目录 一、Jackson 方案&#xff08;SpringBoot默认支持&#xff09;1.1 Jackson 库的特点1.2 Jackson 的核心模块1.3 Maven依赖1.4 代码示例1.5 LocalDateTime 格式化1.6 统一配置1.7 常用注解1.8 自定义序列化和反序列化1.9 Jackson 工具类 二、FastJSON 方案2.1 FastJSON 的特…

OpenHarmony实战开发-MpChart图表实现案例。

介绍 MpChart是一个包含各种类型图表的图表库&#xff0c;主要用于业务数据汇总&#xff0c;例如销售数据走势图&#xff0c;股价走势图等场景中使用&#xff0c;方便开发者快速实现图表UI。本示例主要介绍如何使用三方库MpChart实现柱状图UI效果。如堆叠数据类型显示&#xf…

Niobe WiFi IoT开发板OpenHarmony内核编程开发——Semaphore

本示例将演示如何在Niobe WiFi IoT开发板上使用cmsis 2.0 接口进行信号量开发 Semaphore API分析 osThreadNew() osThreadId_t osThreadNew(osThreadFunc_t func, void *argument,const osThreadAttr_t *attr )描述&#xff1a; 函数osThreadNew通过将线程添加到活动线程列表…

nvm node.js的安装

说明&#xff1a;部分但不全面的记录 因为过程中没有截图&#xff0c;仅用于自己的学习与总结 过程中借鉴的优秀博客 可以参考 1,npm install 或者npm init vuelatest报错 2&#xff0c;了解后 发现是nvm使用的版本较低&#xff0c;于是涉及nvm卸载 重新下载最新版本的nvm 2…

【TCP套接字编程,UDP套接字编程】

文章目录 TCP套接字编程Socket编程Socket 编程TCP套接字编程TCPsocket编程C/S socket 交互: TCP数据结构 sockaddr_in数据结构 hostent UDP套接字编程UDP Socket编程Client/server socket 交互: UDP TCP套接字编程 Socket编程 应用进程使用传输层提供的服务才能交换报文。实现…

解锁创意无限,体验全新Adobe Illustrator 2021 for mac/Win中文版

在数字化创意的浪潮中&#xff0c;Adobe Illustrator 2021中文版无疑是设计师们的得力助手。这款软件集高效、便捷、创新于一体&#xff0c;无论是Mac还是Windows用户&#xff0c;都能在其中找到属于自己的创意空间。 Adobe Illustrator 2021中文版延续了其强大的矢量图形处理…

mybash---打造自己的命令解释器

目前我们Linux的系统默认的命令解释器是bash; 命令解释器&#xff08;也称为命令行解释器或shell&#xff09;是计算机操作系统中的一个重要组件&#xff0c;它负责接收用户输入的命令&#xff0c;并解释和执行这些命令。其实命令解释器就是解析命令,执行命令,输出反馈; 1.命令…

【c 语言】声明了一个指针,会给指针分配内存吗?

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;C语言 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步&…

电商API数据采集接口|跨境卖家在追求精细化运营过程中商品选品以及上架物流几方面应用

跨境电商行业与IT行业资深从业者&#xff0c;知名跨境电商人工智能软件创始人。擅长用RPA机器人技术、aPaaS业务应用平台以及AI人工智能技术为跨境卖家提供新的运营模式和思路&#xff0c;用技术的手段来解决跨境行业的痛点问题。 现在跨境卖家都在追求精细化运营&#xff0c;在…

【Git】Git的安装与常用命令

Git的安装与常用命令 一、Git的安装 &#xff08;一&#xff09;下载 官网下载&#xff1a;https://git-scm.com/downloads 镜像网站&#xff1a;https://registry.npmmirror.com/binary.html?pathgit-for-windows/ &#xff08;二&#xff09;安装 双击安装&#xff0c…

06-vscode+espidf开发调试方法(内置JTAG调试)

使用VS Code和ESP-IDF进行ESP32开发和调试 在我们搭建 IDF 框架后&#xff0c;OpenOCD 已经自动下载好了&#xff0c; 我们通过 JTAG 接口连接使用 OpenOCD 进行调试。而ESP32芯片中内置 了JTAG 电路&#xff0c;无需额外芯片即可调试&#xff0c;更加方便&#xff0c;所以这里…

开源相机管理库Aravis例程学习(三)——注册回调multiple-acquisition-callback

开源相机管理库Aravis例程学习&#xff08;三&#xff09;——回调multiple-acquisition-callback 简介例程代码arv_camera_create_streamArvStreamCallbackTypeArvStreamCallback 简介 本文针对官方例程中的&#xff1a;02-multiple-acquisition-callback做简单的讲解。 ara…

ppt里的音乐哪里来的?

心血来潮&#xff0c;想照着大神的模板套一个类似于快闪的ppt。 ppt里是有一段音乐的&#xff0c;那段音乐就是从幻灯片第二页开始响起的。 但是我就找不到音乐在哪。 甚至我把ppt里的所有素材都删除了&#xff0c;再看动画窗格&#xff0c;仍然是空无一物&#xff0c;显然&…

PyCharm 2024.1 发布:全面升级,助力高效编程!

PyCharm 2024.1 发布&#xff1a;全面升级&#xff0c;助力高效编程&#xff01; 文章目录 PyCharm 2024.1 发布&#xff1a;全面升级&#xff0c;助力高效编程&#xff01;摘要引言 Hugging Face&#xff1a;模型和数据集的快速文档预览针对 JavaScript 和 TypeScript 的全行代…

基于有序抖动块截断编码的水印嵌入和提取算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 噪声测试 旋转测试 压缩测试 2.算法运行软件版本 matlab2022a 3.部分核心程序 ............................................................…

基于深度学习网络的鞋子种类识别matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 load gnet.mat % 使用训练好的网络对验证数据进行分类预测 [Predicted_Label, Probability] c…

基于Canvas实现的简历编辑器

基于Canvas实现的简历编辑器 大概一个月前&#xff0c;我发现社区老是给我推荐Canvas相关的内容&#xff0c;比如很多 小游戏、流程图编辑器、图片编辑器 等等各种各样的项目&#xff0c;不知道是不是因为我某一天点击了相关内容触发了推荐机制&#xff0c;还是因为现在Canvas…

开源模型应用落地-chatglm3-6b-批量推理-入门篇(四)

一、前言 刚开始接触AI时&#xff0c;您可能会感到困惑&#xff0c;因为面对众多开源模型的选择&#xff0c;不知道应该选择哪个模型&#xff0c;也不知道如何调用最基本的模型。但是不用担心&#xff0c;我将陪伴您一起逐步入门&#xff0c;解决这些问题。 在信息时代&#xf…

python将pdf转为docx

如何使用python实现将pdf文件转为docx文件 1.首先要安装pdf2docx库 pip install pdf2docx2.实现转换 from pdf2docx import Converterdef convert_pdf_to_docx(input_pdf, output_docx):# 创建一个PDF转换器对象pdf_converter Converter(input_pdf)# 将PDF转换为docx文件pdf…