排序算法:插入排序(直接插入排序、希尔排序)

news2024/12/26 11:39:23

朋友们、伙计们,我们又见面了,本期来给大家解读一下有关排序算法的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

目录

 

前言:

1.排序的概念及其运用

1.1排序的概念

1.2排序的应用

1.3常见的排序算法

2.排序算法的实现

2.1插入排序

2.1.1基本思想

2.1.2直接插入排序

#直接插入排序完整代码:

2.1.3希尔排序 

#预排序

#直接插入排序

#希尔排序完整代码:

3.算法的效率比较


 

前言:

排序无处不在,在生活中我们无时无刻都在间接或者直接的使用排序这个方法,很多复杂的事情在经过排序之后都会变得简单许多。那么,在现阶段我们学习的数据结构一这块排序又该怎么实现呢?

1.排序的概念及其运用

1.1排序的概念

排序
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小递增或递减的排列起来的操作。
稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2排序的应用

比如我们电脑桌面上的应用的排序方式:

比如我们网购的时候也可以对商品进行排序:

比如2023中国高校排行榜:

 等等都是通过排序来展示出来的,所以可见排序无论是在哪个领域都应用广泛。

1.3常见的排序算法

如果大家想要测试自己的排序算法正确与否可以在这个OJ链接来进行测试:https://leetcode.cn/problems/sort-an-array/

2.排序算法的实现

2.1插入排序

2.1.1基本思想

直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为 止,得到一个新的有序序列
这其实就跟我们玩扑克牌一样,摸牌、插入:

2.1.2直接插入排序

当插入第 i(i>=1) 个元素时,前面的 array[0],array[1],…,array[i-1] 已经排好序,此时用 array[i] 的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将 array[i] 插入,原来位置上的元素顺序后移。
排升序:简单的总结就是在[0, end]这个区间中插入tmp,从end的位置开始比较,如果tmp小于end这个位置的元素,那么就将end往后面挪,然后与end-1这个元素再作比较(前提是:[0, end] 这个区间的元素有序)。

我们先来写出一趟插入排序的基本逻辑:

排升序:在[0,end]区间插入tmp,先比较tmp与end的大小关系,如果比end小,那么将end往后面挪一个,在用tmp和end-1比较,如果比end-1小,再将end-1往后挪,直到tmp比[0,end]区间中那个元素大,那么就停止,如果tmp比这个区间的所有数都要小,那么直接将tmp放在第一个位置即可。

代码演示: 

//直接插入排序
//在[0,end]插入tmp
//升序
void InsertSort(int* a, int n)
{
    //一趟插入排序
	int end;
	int tmp;
    
	while (end >= 0)
	{
		if (a[end] > tmp)
		{
            //挪数据
			a[end + 1] = a[end];
			end--;
		}
		else
		{    
			break;
		}
	}
    //放数据
    //走到这里有两种情况:
    //1.tmp比a[end]大
    //2.tmp比a中的任何数都小
    a[end + 1] = tmp;
}

完成了一趟的插入排序,那么怎么样实现在数组中排序呢?

第一次取数组的第一个元素直接放进去即可,第二次就需要和第一次放进去的元素进行比较,然后排成有序,也就是说我们可以将第一个元素看成有序,然后把后面的依次插入,所以呢,我们可以将已插入的数据看作是手里的手牌,未插入的数据看作牌堆的牌,我们每一次摸一张插入一张,这样子就实现了使用插入排序排序数组

#直接插入排序完整代码:

//直接插入排序
//在[0,end]插入tmp
//升序
void InsertSort(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		//一趟插入排序
		int end = i - 1;
		int tmp = a[i];

		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				//挪数据
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		//放数据
		//走到这里有两种情况:
		//1.tmp比a[end]大
		//2.tmp比a中的任何数都小
		a[end + 1] = tmp;
	}
}
直接插入排序的特性总结:
1. 元素集合越接近有序直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)(最好的情况可以做到O(N))
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定

直接插入排序和冒泡排序比较:

通过上面的实现过程还看不出来插入排序的速度,我们可以和之前学过的  冒泡排序作一比较

冒泡排序的时间复杂度也为O(N^2),那么使用它们两个算法分别来排升序一个原本就是升序的数组,那么即便数据一次都不需要挪动,冒泡排序还需要O(N^2),但是直接插入排序只需要O(N)。所以直接插入排序在某种特殊情况下还是蛮快的。

2.1.3希尔排序 

希尔排序也被称为缩小增量排序

希尔排序法的基本思想是:
1.预排序:接近有序
2.插入排序

整体步骤就是确定一个整数值gap,然后间隔为gap,将所有数据分为gap组,对着gap组数据分别进行插入排序,最后再整体来一次插入排序,这个预排序的目的就是为了让数据接近有序,以便减少最后一次插入排序的时间。
例如要将:9 8 7 6 5 4 3 2 1 0 排成升序

#预排序

接下来我们就用代码来演示:

我们先不管gap怎么设置,在这里我们就先将gap设置为3

还是使用上面的例子:9 8 7 6 5 4 3 2 1 0 排成升序

1.先将这组数据分为gap组,然后分别对这gap组数据进行插入排序。

①我们先来进行一次排序,这次类比之前的直接插入排序不一样的是:先得保存下一个数据,因为往后面挪动数据会进行覆盖,然后每一次比较的是间隔为gap的数据:

代码演示:

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

	//一次插入排序
	int end;
	//保存下一个位置的数据
	int tmp = a[end + gap];
	//比较、挪动
	while (end >= 0)
	{
		if (a[end] > tmp)
		{
			a[end + gap] = a[end];
			end -= gap;
		}
		else
		{
			break;
		}
	}
	a[end + gap] = tmp;
}

②接着我们如何将这gap组中的其中一组进行排序呢?

这里要注意一个问题,对其中一组数据进行插入排序时每一次跳过的不是1,而是gap,并且end在往后面走的过程中不能大于n - gap,如果大于n - gap,那么tmp就会造成越界访问。

代码演示:

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

	//              控制条件得注意、 步长要注意
	for (int i = 0; i < n - gap; i += gap)
	{
		//一次插入排序
		int end = i;
		//保存下一个位置的数据
		int tmp = a[end + gap];
		//比较、挪动
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			else
			{
				break;
			}
		}
		a[end + gap] = tmp;
	}
}

③进行完其中一组的排序,接下来就要进行gap组分别排序,如果我们仔细观察不难发现,当end为0的时候排序的是第一组,当end为1的时候排序的是第二组,那么我们可以再设置一个循环,以gap为控制循环条件:

代码演示:

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

	//循环gap组,对gap组的数据分别进行插入排序
	for (int j = 0; j < gap; j++)
	{
		//              控制条件得注意、 步长要注意
		for (int i = j; i < n - gap; i += gap)
		{
			//一次插入排序
			int end = i;
			//保存下一个位置的数据
			int tmp = a[end + gap];
			//比较、挪动
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

上面的代码用了三层循环,看起来是比较复杂的,但是我们可以进行改版一下,大家注意看代码的变化:

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

	//对gap组数据分别进行插入排序
	for (int i = 0; i < n - gap; i++)
	{
		//一次插入排序
		int end = i;
		//保存下一个位置的数据
		int tmp = a[end + gap];
		//比较、挪动
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			else
			{
				break;
			}
		}
		a[end + gap] = tmp;
	}
}

很多老铁在这里看不懂改版之后代码,小编在这里带大家解释一下:首先上面的这两种代码的所需要的时间是完全没有区别的,如果使用三层循环,比较容易理解,改版之后的代码比较难理解,三层循环采用的是分组排序,两层循环采用的是多组并排(当i == 0时排序的是第一组第一个元素,当i == 1时排序的是第二组的第一个元素......以此类推就将这全部的数据排完了),这样解释应该就不难理解。

 这里采取自愿,两种方法的效率都是一样,没有区别,按照个人喜欢来自取。

#直接插入排序

当把gap组数据分别插入排序之后,还需要进行最后一次插入排序,那么最直接的方法就是直接在上面的代码的最后面再调用一次普通的插入排序即可,但是这未免有点太麻烦了,所以我们需要从gap下手,我们可以先来比较一下插入排序和希尔排序:

gap在这里决定了每一次的步长,所以:

gap越大,大的数可以更快的到后面,小的数可以更快的到前面,但是数据越不接近有序。

gap越小,大的数和小的数移动的比较缓慢,但是数据在移动完之后越接近有序。

gap == 1时,就是直接插入排序。

在上面排序10个数据的时候gap为3是比较合适的,但是如果要排序10000个数据,gap还是为3的话就有点挫了,所以gap的设定要根据元素个数来进行确定,然后在排序到最后一次时gap得是1,为了完成最后一次的直接插入排序。

#希尔排序完整代码:

//希尔排序
void ShellSort(int* a, int n)
{
	//1. gap > 1:预排序
	//2. gap == 1 :直接插入排序

	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		//对gap组数据分别进行插入排序
		for (int i = 0; i < n - gap; i++)
		{
			//一次插入排序
			int end = i;
			//保存下一个位置的数据
			int tmp = a[end + gap];
			//比较、挪动
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们后面可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定,在某些大佬的书籍中就提到过:
《数据结构 (C 语言版 ) --- 严蔚敏

 

《数据结构 - 用面相对象方法与 C++ 描述》 --- 殷人昆

 所以呢,希尔排序的时间复杂度是O(N^{3/2})

4. 稳定性:不稳定

3.算法的效率比较

 在这里我们来比较我们已学过的冒泡排序、堆排序、直接插入排序和希尔排序的效率,再比较之前我们再来回顾一下冒泡排序和堆排序:

//冒泡排序
void BubbleSort(int* a, int n)
{
	//设置冒泡排序的趟数
	for (int i = 0; i < n - 1; i++)
	{
		//一趟冒泡排序的次数
		bool exchange = false;
		for (int j = 1; j < n - j; j++)
		{
			if (a[j] > a[j + 1])  //如果前面的一个数字大于后面的数字就交换
			{
				int tmp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = tmp;
				exchange = true;
			}
		}
		if (exchange == false)
		{
			break;
		}
	}
}
//堆排序

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

//向下调整
void AdjustDown(int* a, int n, int parent)
{
	//假设左孩子为左右孩子中最小的节点
	int child = parent * 2 + 1;

	while (child < n)  //当交换到最后一个孩子节点就停止
	{
		if (child + 1 < n  //判断是否存在右孩子
			&& a[child + 1] < a[child]) //判断假设的左孩子是否为最小的孩子
		{
			child++;   //若不符合就转化为右孩子
		}
		//判断孩子和父亲的大小关系
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			//更新父亲和孩子节点
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//O(N * logN)
void HeapSort(int* a, int n)
{
	//建堆--向下调整算法建堆
	//时间复杂度为O(N)
	for (int i = ((n - 1) - 1) / 2; i >= 0; --i)
	{//       这里的n-1表示最后一个叶子节点
		//       最后一个叶子节点的父亲就是:
		//           (n-1)-1/2;
		AdjustDown(a, n, i);
	}

	//O(N * logN)
	int end = n - 1;
	while (end > 0)
	{
		//交换堆顶和最后一个数据的位置
		Swap(&a[0], &a[end]);
		//向下调整,找次小的
		AdjustDown(a, end, 0);
		end--;
	}
}

接下来我们可以使用一段代码来测试一下上面的这四种算法的效率区别。

采用的思路就是随机生成1w个数据,然后使用这些算法进行排序:

代码演示:

 

// 测试排序的性能对比
void TestOP()
{
	srand(time(0));
	const int N = 100000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);

	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
	}
	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();
	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();
	int begin3 = clock();
	BubbleSort(a3, N);
	int end3 = clock();
	int begin4 = clock();
	HeapSort(a4, N);
	int end4 = clock();
	int begin5 = clock();

	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("BubbleSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
}

在这里测试的时候建议大家使用Release版本,测试效果会更好(这里测试的单位都是毫秒):

 

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!

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

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

相关文章

CQ 社区版 v2.1.0 发布 | 新增数据发布变更、内置脱敏规则等功能

Hello&#xff0c;社区的小伙伴们&#xff0c;又到了每月版本发布时间。&#x1f389;&#x1f389;&#x1f389; 本次社区版更新带来了新功能 「发布变更」&#xff0c;以及内置脱敏规则、授权粒度细化、连接池管理、变更链接密钥等&#xff0c;信息量不少&#xff0c;一起来…

在生信中利用Chat GPT/GPT4

论文链接Ten Quick Tips for Harnessing the Power of ChatGPT/GPT-4 in Computational Biology | Papers With Code 之前在paper with code上比较火的一篇文章&#xff0c;最近要给生科的学长学姐们个分享所以把这个翻了翻&#xff0c;原文自认为废话比较多&#xff0c;于是选…

基于Java物流管理系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

【图书推荐 | 14】后端系列

【赠书活动第十四期 】 图书推荐 本期书籍&#xff1a;后端系列 图书列表 本期图书列表&#xff1a; Spring Cloud 微服务快速上手项目驱动零起点学JavaNode.js 从基础到项目实战Diango Web 开发实例精解Flask Web 全栈开发实战精通Hadoopsmysql 数据库基础与实战应用Neo4j 图谱…

ChatGLM-6B云服务器部署教程

目录 一、准备服务器1.购买服务器2.开机进入终端3.进入终端 二、部署ChatGLM1.执行命令2.本地代理访问地址2.1 结果如下2.2 api接口一样操作 三、Fastapi流式接口1.api_fast.py1.2 将api_fast.py上传到服务器 2.准备插件3.访问地址 博客园地址&#xff1a;https://www.cnblogs.…

【裸机开发】中断系统 —— IRQ 中断服务函数(汇编部分)

IRQ 和前面的Reset 函数不大一样&#xff0c;当一个IRQ中断产生时&#xff0c;我们也不知道这个IRQ中断来自哪个外设&#xff0c;因此&#xff0c;需要先获取到中断ID&#xff0c;随后才会跳转到真正的中断服务函数执行处理逻辑。 整个 IRQ 中断处理可以看做是包含了两个部分&…

CSS查缺补漏之选择器

最近在复盘CSS基础知识&#xff0c;发现很多CSS选择器里面还是大有学问&#xff0c;需要详细总结一番&#xff0c;以备差缺补漏~ 作为CSS基础的一大类别&#xff0c;选择器又分为多种类别&#xff0c;本篇内容默认读者已了解并掌握基础选择器【通配符选择器】、【元素选择器】…

springboot+vue项目之MOBA类游戏攻略分享平台(java项目源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的MOBA类游戏攻略分享平台。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xf…

自动化测试框架Playwright安装以及使用

最近&#xff0c;微软开源了一个非常强大的自动化项目叫 playwright-python 它支持主流的浏览器&#xff0c;包含&#xff1a;Chrome、Firefox、Safari、Microsoft Edge 等&#xff0c;同时支持以无头模式、有头模式运行&#xff0c;并提供了同步、异步的 API&#xff0c;可以…

C++学习——第二节课-输入输出

大家好&#xff0c;我是涵子。今天我们来学习C中的输入输出。 一、电脑中的输入输出 日常生活中常见的电脑、手机、电视机外部接口&#xff0c;也就是I/O&#xff08;输入/输出&#xff09;接口部分&#xff0c;其样式、种类较多&#xff0c;不同的接口配置也体现了设备的档次…

SpringBoot整合jwt+redis+随机验证码+Vue的登录功能

一、运行效果展示 &#xff01;注意&#xff1a;前端的Vue项目中要引入element-ui和axios # npm安装element-ui、axios npm insatll element-ui -S npm install axios -S # 在main中引入 // 引入ElementUI import ElementUI from element-ui import element-ui/lib/theme-chalk…

springboot动态加载json文件

resources下面的配置文件&#xff0c;application文件修改启动会实时加载新的内容 其他的文件属于静态文件&#xff0c;打包后会把文件打入jar里面&#xff0c;修改静态文件启动不会加载新的内容 Resource areacode nre FileSystemResource("config" File.separa…

技术分享——数据安全之数据分类方法小集

背景 2021年6月10日&#xff0c;《中华人民共和国数据安全法》&#xff08;以下简称“《数安法》”&#xff09;通过了第十三届全国人民代表大会常务委员会第二十九次会议并予以发布&#xff0c;标志着我国数据安全工作进入到有法可依的新阶段。 本文通过梳理现有的部分法规、…

Fiddler Orchestra从安装到实战演练

上次谈到了Fiddler Orchestra用户指南&#xff0c;这次笔者把自己的实战演练分享大家&#xff0c;闲话少说&#xff0c;步骤如下&#xff1a; 1、根据前面文章《Fiddler Orchestra用户指南》&#xff0c;Fiddler Orchestra客户端和控制器只能运行在至少支持.NET Standard 2.0的…

Redis入门(一)

第1章 NoSQL 1.1 NoSQL数据库 1.1.1 NoSQL是什么 &#xff08;1&#xff09;NoSQL(Not Only SQL )&#xff0c;意即“不仅仅是SQL”&#xff0c;泛指非关系型的数据库。 &#xff08;2&#xff09;NoSQL不拘泥于关系型数据库的设计范式&#xff0c;放弃了通用的技术标准&…

MyAQL事务

目录 ----------------------MySQL 事务-------------------------------- 1&#xff0e;事务的概念 2&#xff0e;事务的ACID特点 ●原子性 ●一致性 ●隔离性 事务隔离级别的作用范围分为两种&#xff1a; ●持久性 3&#xff0e;事务控制语句 案例&#xff1a; 4…

【Elacticsearch】 倒排索引的查增删改原理

关联文章&#xff1a;【Elacticsearch】 原理/数据结构/面试经典问题整理_东方鲤鱼的博客-CSDN博客 建立索引的原理 当向协调节点发送请求以索引新文档时&#xff0c;将执行以下操作&#xff1a; 所有在Elasticsearch集群中的节点都包含&#xff1a;有关哪个分片存在于哪个节点…

深度学习入门笔记1--梯度下降之--为什么是负方向--为什么局部下降最快的是负梯度方向

本节目标理解梯度下降的原理&#xff0c;主要围绕以下几个问题展开&#xff1a; 梯度下降法的用途&#xff1f;什么是梯度&#xff1f;为什么是负的梯度为什么局部下降最快的方向就是梯度的负方向。 需要的知识储备&#xff1a;一级泰勒展开公式 向量内积计算公式 1. 梯度下…

Sui主网升级至V1.3.0版本

Sui主网现已升级至V1.3.0版本&#xff0c;升级要点如下所示&#xff1a; 将协议版本更新至12 开始在Narwhal中使用BatchV2&#xff0c;新增VersionedMetadata允许更精细的追踪Narwhal批处理延迟。有关详细信息&#xff0c;请参阅#12178和#12290。 将协议版本更新至13 弃用0…

wtmp日志读取

wtmp日志介绍 之前遇到一个AIX服务器登录不上&#xff0c;但是能ping通的事情。一开始我怀疑是sshd服务坏掉了&#xff0c;但是使用telnet也无法登录。好在这台机器所在的机房就在我隔壁&#xff0c;于是外接显示器&#xff0c;直接上机操作。好在直接通过物理介质还是能登录得…