乐观的调优(插入排序希尔排序)

news2024/11/15 23:00:03

学习目标

写在前面

1.插入排序

2.插入排序实战 

3.插入排序的实现 

4.插入排序的效率

5.平均情况

6.希尔排序

7.希尔排序的实现

8.希尔排序的效率

9.总结


写在前面

之前我们衡量一个算法的效率时,都是着眼于它在最坏情况下需要多少步。原因很简单,连最坏的情况都做足准备了,其他情况自然不在话下。

但是,在我们实际生活中并不总是面临最坏情况,更多的是平均情况。本章我们会见证一种自适应性极强的排序算法---希尔排序,还有它的组成它的关键---插入排序


1.插入排序

我们已经学过两种排序算法:冒泡排序选择排序。虽然它们的效率都是 O(N^2 ),但其实选择排序比冒泡排序快一倍。

运用大O给代码提速(冒泡排序)http://t.csdn.cn/TByoR用或不用大O来优化代码(选择排序)http://t.csdn.cn/worMo

现在来学第三种排序算法——插入排序(直接插入排序)。你会发现,顾及最坏情况以外的场景将是多么有用。插入排序包括以下步骤。

插入排序包括以下步骤。

(1) 在第一轮里,暂时将索引 1(第 2格)的值移走,并用一个临时变量来保存它。这使得该索引处留下一个空隙,因为它不包含值。

 在之后的轮回,我们会移走后面索引的值。

(2) 接着便是平移阶段,我们会拿空隙左侧的每一个值与临时变量的值进行比较。

如果空隙左侧的值大于临时变量的值,则将该值右移一格。 

随着值右移,空隙会左移。如果遇到比临时变量小的值,或者空隙已经到了数组的最左端,就结束平移阶段。 

(3) 将临时移走的值插入当前空隙。

(4) 重复第(1)至(3)步,直至数组完全有序。


2.插入排序实战 

下面尝试对 [4, 2, 7, 1, 3] 数组运用插入排序。

第 1轮先从索引 1开始,其值为 2。

准备工作:暂时移走 2,并将其保存在变量 tmp 中。图中被移到数组上方的就是
tmp。 

 第 1步:比较 4与 tmp中的 2。

第 2步:因为 4大于 2,所以把 4右移。

 于是空隙移到了数组最左端,没有其他值可以比较了。

 第 3步:将 tmp插回数组,完成第一轮。

开始第 2轮。

准备工作:暂时移走索引 2的值,并保存到 tmp中。于是 tmp等于 7。

第 4步:比较 4与 tmp。

4小于 7,所以无须平移。因为遇到了小于 tmp的值,所以平移阶段结束。

第 5步:将 tmp插回到空隙中,结束第 2轮。

开始第 3轮。

准备工作:暂时移走 1,并将其保存到 tmp中。

 第 6步:比较 7与 tmp。

 第 7步:7大于 1,于是将 7右移。

 第 8步:比较 4与 tmp。

第 9步:4大于 1,于是也要将 4右移。 

第 10步:比较 2与 tmp。

第 11步:2比较大,所以将 2右移。

第 12步:空隙到了数组最左端,因此我们将 tmp插进去,结束这一轮。 

开始第 4轮。

准备工作:暂时移走索引 4的值 3,保存到 tmp中。

第 13步:比较 7和 tmp。

 第 14步:7更大,于是将 7右移。

第 15步:比较 4与 tmp。

第 16步:4大于 3,所以将 4右移。

第 17步:比较 2与 tmp。2 小于 3,于是平移阶段完成。

 第 18步:把 tmp插回到空隙。

 至此整个数组都排好序了。


3.插入排序的实现 

以下使用C语言实现的直接插入排序:

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp)         //大于tmp,往后挪一个
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;          //把tmp插入空隙
	}
}

 让我们一步步来讲解:

for (int i = 0; i < n - 1; i++)

 最外层的这个循环用来控制end的位置,也就是一个轮回。

		int end = i;
		int tmp = a[end + 1];

我们通过控制end的位置,使end与end之前的数列都是有序的,而把end+1索引处的值(也就是tmp)插入到end之前的数列中。所以,end的值是从0开始的。这样能保证end与end之前的数列是有序的(因为只有一个数),那么将tmp插入后,前end+1个数都是有序的,再依次执行下去。

while (end >= 0)

end索引处的值会发生移动,最坏的情况是tmp的值比之前的有序数列中每一个值都要小,那么空隙的位置就在end=0处。例如:

            if (a[end] > tmp)         //大于tmp,往后挪一个
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}

找到空隙,将比tmp大的数字不断往后挪,直到找到小于等于tmp的数字。

a[end + 1] = tmp;          //把tmp插入空隙

将tmp插入空隙。


4.插入排序的效率

插入排序包含 4种步骤:移除、比较、平移和插入。要分析插入算法的效率,就得把每种步骤都统计一遍。

首先看看比较。每次拿 tmp跟空隙左侧的值比大小就是比较。

在数组完全逆序的最坏情况下,我们每一轮都要将 tmp左侧的所有值与tmp比较。因为那些值全都大于 tmp,所以每一轮都要等到空隙移到最左端才能结束。

对于含有N个元素的数组,可以得出比较的总次数为:

1 + 2 + 3 + … + N - 1 次。

接下来看看其他几种步骤。

我们每次将值右移一格,就是平移操作。当数组完全逆序时,有多少次比较就要多少次平移,因为每次比较的结果都会使你将值右移。

因而可以得出平移的总次数为:

1 + 2 + 3 + … + N - 1 次。

tmp的移除跟插入在每一轮里都会各发生一次。因为总是有 N - 1轮,所以可以得出结论:有 N - 1次移除和 N - 1次插入。

把它们都相加。

N^2 比较和平移的合计
+ N - 1 次移除
+ N - 1 次插入
=
N^2 + 2N - 2

我们已经知道大 O有一条重要规则——忽略常数,于是你可能会将其简化成 O(N^2 + N)。不过,现在来学习一下大 O的另一条重要规则:

大 O 只保留最高阶的 N

换句话说,如果有个算法需要 N^4 + N^3 + N^2 + N步,我们就只会关注其中的 N^4 ,即以 O(N^4 )
来表示。为什么呢?

请看下表。

随着 N的变大,N^4 的增长越来越抛离其他阶。当 N为 1000时,N^4 就比 N^3 大了 1000倍。因
此,我们只关心最高阶的 N。
所以在插入排序的例子中,O(N^2 + N)还得进一步简化成 O(N^2 )。

不过上一章曾指出,虽然冒泡排序和选择排序都是 O(N^2 ),但选择排序实际上是 N^2 / 2步,
比 N 2 步的冒泡排序更快。乍一看,你可能会觉得插入排序跟冒泡排序一样,因为它们都是 O(N^2 ),其实插入排序是 N^2 + 2N - 2步。你或许会认为比冒泡排序和插入排序快一倍的选择排序是三者中最优的,但事情并没有这么简单。


5.平均情况

确实,在最坏情况里,选择排序比插入排序快。但是我们还应该考虑平均情况。

最好情况和最坏情况很少发生。现实世界里,最常出现的是平均情况。

这是很有道理的。你设想一个随便洗乱的数组,出现完全升序或完全降序的可能性有多大?最可能出现的情况应该是随机分布。

下面试试在各种场景中测试插入排序。

完全降序的最坏情况之前已经见过,它每一轮都要比较和平移所遇到的值(这两种操作合计N^2 步)。

对于完全升序的最好情况,因为所有值都已在其正确的位置上,所以每一轮只需要一次比较,完全不用平移。

最坏情况是所有数据都要比较和平移;最好情况是每轮一次比较、零次平移;对于平均情况,总的来看,是比较和平移一半的数据。

如果说插入排序的最坏情况需要 N 2 步,那么平均情况就是 N 2 / 2步。尽管最终大 O都会写成 O(N^2 )。

可以看到插入排序的性能在不同场景中差异很大。最坏、平均、最好情况,分别需要 N^2 、
N^2 / 2、N步。

那么哪种算法更好?选择排序还是插入排序?答案是:看情况。对于平均情况(数组里的值随机分布),它们性能相近。如果你确信数组是大致有序的,那么插入排序比较好。如果是大致逆序,则选择排序更快。


6.希尔排序

希尔排序是对插入排序做了简单的优化,却产生了质的飞跃。直接让不太起眼的插入排序比肩闻名算法界的快速排序。我们知道对于插入排序,数据的元素越是接近有序,那么它的效率就越高;对于完全有序的数组,它甚至可以快到O(N)。

那么希尔排序的主要思想是,我们不断地对数组进行预排序,使数组里大的元素尽量到数组的后面,小的元素尽量到数组的前面。

完成对数组的预排序看,我们采取的方法是对数组进行分组,把数组里间隔相同长度的元素划分为一组。例如:

 接下来我们对每一组的元素进行排序。

如图所示,第一趟排序,较大的元素已经到了数组的后面了。接下来再次重新分组,再次预排序:

经过第二趟预排序,我们发现数组已经大致接近有序了,那么最后一次,我们取间隔为1分组,其实就是一次普通的插入排序:

至此,数组已经完全有序了。

通过上面的例子不难看出,希尔排序就是对插入排序的简单优化,引入了预排序的概念。

当gap>1时,进行的是预排序(也就是对每一组进行插入排序);

当gap=1时,进行对整个数组的插入排序。


7.希尔排序的实现

下面使用C语言实现的希尔排序:

void ShellSort(int* a, int n)
{
	int gap = n;             //间隔
	while (gap > 1)
	{
		gap = gap / 3 + 1;   //+1是为了使gap最后等于1
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

代码相较于插入排序并没有改变多少,在插入排序的基础上多了一层循环用来控制gap。

此处可以看出,预排序并不是固定的3次或者4次。而是取决于数组元素个数,这么做一是为了方便控制gap,二是预排序越多次,对整体排序而言更有利,所以我们不用吝啬预排序,并不是预排序越多,花费的步数就越多,时间复杂度就越高。

gap = gap / 3 + 1; 

除了这样控制gap外,还有一种常用的方法:

gap = gap / 2; 

但是根据有人实验得出,第一种方式比第二种方式快一点点,但是差距很细微,所以两者皆可。


8.希尔排序的效率

希尔排序的时间复杂度并不好算,因为每次取gap的值都不同,而且gap取不同值的情况下,每次预排序所面临的数据也不同。希尔排序的时间复杂度计算需要运用到数学知识,而且目前为止我们也没有得到严格的标准答案。有人在大量实验的基础上得出希尔排序的时间复杂度接近O(N^1.3)。

《数据结构(C语言版)》--- 严蔚敏

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

因为我们的gap取值的方式是按照Knuth的方式取的,所以以我们这种方式实现的希尔排序时间复杂度暂定为O(N^1.25)。

目前仅仅靠时间复杂度的对比,我们也许感受不到什么叫做质的飞跃,那我们就通过一组数据来对比一下二者的差距。

这是一个测试排序算法所用时间的函数:

void TestOP()
{
	srand(time(0));
	const int N = 1000000;
	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);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	int* a7 = (int*)malloc(sizeof(int) * N);

	int j = 0;
	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];
		a6[i] = a1[i];
		a7[i] = a1[i];
	}
	printf("%d\n", j);

	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

	int begin3 = clock();
	//SelectSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	//HeapSort(a4, N);
	int end4 = clock();

	int begin7 = clock();
	//BubbleSort(a7, N);
	int end7 = clock();

	int begin5 = clock();
	//QuickSort(a5, 0, N - 1);
	int end5 = clock();

	int begin6 = clock();
	//MergeSort(a6, N);
	int end6 = clock();

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

	printf("QuickSort:%d\n", end5 - begin5);
	printf("MergeSort:%d\n", end6 - begin6);

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

我们以排序1百万个元素为例,得到以下结果:

 很显然两种排序算法已经不在一个数量级了。


9.总结

懂得区分最好、平均、最坏情况,是为当前场景选择最优算法以及给现有算法调优以适应环境变化的关键。记住,虽然为最坏情况做好准备十分重要,但大部分时间我们面对的是平均情况。

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

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

相关文章

PyQt6快速入门-开发环境搭建

PyQt6开发环境搭建 文章目录 PyQt6开发环境搭建1、软件安装2 、创建工程2.1 创建Qt工程2.2 创建Python工程2.3 PyQt6启动失败问题解决本文将介绍如何在Windows下创建PyQt6的开发环境。 1、软件安装 第一步,下载并安装Python环境 https://www.python.org/downloads/windows/或…

effective C++读书笔记3

目录 在资源管理中小心copy行为 在资源管理中提供对原始资源的访问 以独立语句将newed对象置入智能指针 这是effectiveC中第三章内容&#xff1a;资源管理 在资源管理中小心copy行为 不是所有资源都是heap-based&#xff0c;对那种资源而言&#xff0c;像auto_ptr和 tr1 :…

【C语言进阶】数据在内存的存储

作者:匿名者Unit 目录一.数据类型分类1.整形2.浮点型3.构造类型4.指针类型5.空类型二.整形的存储1.原、反、补码2.大小端存储三.浮点数的存储一.数据类型分类 根据不同的数据类型&#xff0c;在内存中的人存储方法也有所差异&#xff0c;所以我们先来介绍一下数据类型的分类。…

(二)uboot移植--从零开始自制linux掌上电脑(F1C200S)<嵌入式项目>

目录 一、前言 二、F1C200s上电启动顺序 三、前期准备 四、新建用户 五、交叉编译环境配置 六、uboot简介 七、uboot移植 &#x1f34f; uboot下载 &#x1f34f; uboot默认配置 &#x1f34f; uboot图形界面配置 &#x1f34f; uboot编译 &#x1f34f; 烧录bin…

Python-变量和简单数据类型

目录 1. 字符串 1.1. 修改字符串大小写 1.2. 合并字符串 1.3. 制表符与换行符 1.4. 删除空白 2. 数字 2.1. 使用 2.2. 使用str函数避免类型错误 2.3. 注释 2.4. Python之禅 1. 字符串 1.1. 修改字符串大小写 首字母大写&#xff1a;name.title()全部大写&#xf…

基于DDAUNet的CT食管肿瘤分割

摘要 在CT图像中手动或自动描绘食道肿瘤是非常具有挑战性的。这是由于肿瘤与邻近组织的对比度低,食道的解剖结构变化,以及偶尔存在异物(如喂食管)。 本文提出了一种基于卷积神经网络的全自动端到端食管肿瘤,本文所提出的网络称为扩张密集注意力网络,利用每个密集块中的空…

VScode代码片段尝试

最近开始尝试强迫自己使用 VSCode&#xff0c;毕竟 Webstorm 用了很多年了&#xff0c;依赖性比较严重。但是&#xff0c;最近 Webstorm 越来越难搞到免费永久合法使用方式了。 遂转向 VSCode 。 不过 VSCode 需要调教&#xff0c;不像 Webstorm 那么集成度比较高&#xff0c…

CalDAV网络服务器Baikal

什么是 Baikal &#xff1f; Baikal 是一个免费的开源自托管 CalDav 和 CardDav 服务器&#xff0c;适用于想要管理其数据并确保其数据是私有的用户。 Baikal和群晖套件中的 Synology Calendar 是类似的应用&#xff1b; 什么是 CalDav ? CalDav 是一种互联网标准和协议&…

Qt扫盲-QVector理论总结

QVector理论总结一、概述二、使用1. 声明初始化2. 获取元素和链表信息3. 常用操作4. 迭代Vector三、注意一、概述 QVector是Qt的泛型容器类之一。它将每一个元素存储在相邻的内存位置&#xff0c;并提供快速的基于索引的访问。 QList&#xff0c; QLinkedList&#xff0c; QVe…

eurake原理分析以及搭建

消费者随时能够监控到服务的状态 消费者如何获取服务提供者具体的信息&#xff1f; 1&#xff09;服务提供者启动时间eureka注册自己的信息 2&#xff09;eureka保存这些信息 3&#xff09;消费者根据服务名称向eureka拉取提供者的信息 如果有多个服务提供者&#xff0c;消费者…

VS代码生成工具ReSharper v2022.3官宣首发——支持C# 11

实质上&#xff0c;ReSharper特征可用于C#&#xff0c;VB.net&#xff0c;XML&#xff0c;Asp.net&#xff0c;XAML&#xff0c;和构建脚本。 使用ReSharper&#xff0c;你可以进行深度代码分析&#xff0c;智能代码协助&#xff0c;实时错误代码高亮显示&#xff0c;解决方案范…

网络OSI(七层模型)

OSIOSI是一个理论上的网络通信模型&#xff0c;而TCP/IP则是实际运行的网络协议。TCP/IPTCP/IP (传输控制协议/网际协议) 网络通信模型 以及一整个网络传输协议家族应用层应用层协议: FTP TFTP HTTP SNMP SMTP DNS Telnet表示层 数据格式化 代码转换 数据解密/加密会话层 解除或…

低代码如何构建响应式布局前端页面

“你开发的界面为啥在我的屏幕里这么小啊&#xff1f;” “这个界面为啥在我这里会出现横向滚动条啊&#xff1f;” 大家在进行前端界面开发时&#xff0c;有没有遇到这些类似的问题呢&#xff1f;又是如何解决的呢&#xff1f; 页面响应式 在进行项目交付的场景中&#xf…

JavaEE【Spring】:Spring AOP

文章目录一、概念1、定义2、作用二、Spring AOP1、AOP 组成① 切面&#xff08;Aspect&#xff09;② 连接点&#xff08;Join Point&#xff09;③ 切点&#xff08;Pointcut&#xff09;④ 通知&#xff08;Advice&#xff09;2、实现① 添加 AOP 框架支持② 定义切面和切点Ⅰ…

都有哪些好用的设备维护管理软件?这5款值得一试

都有哪些好用的设备维护管理软件&#xff1f; 速速点进来&#xff0c;这些超高性价比的设备维护管理App/软件不容错过&#xff01; 设备是工厂及企业生存的基础&#xff0c;设备的正常运作是保证工厂及企业存活的前提&#xff0c;而要保证设备的正常运作&#xff0c;就必须要…

NLP预训练小结-从词向量到BERT

图像预训练预训练首先是在图像领域广泛应用的。设计网络结构后&#xff0c;处理图像一般是CNN的多层叠加网络结构&#xff0c;先用训练集对网络预先训练&#xff0c;处理新任务时采取相同的网络结构&#xff0c;在比较浅的几层CNN结构&#xff0c;网络参数初始化的时候可以加载…

思科路由器DHCPv6中继服务配置

配置如下&#xff1a; Router>ena Router#conf t Router(config)#host R1 R1(config)#ipv6 unicast-routing R1(config)#service dhcp R1(config)#int g0/1 R1(config-if)#ipv6 add 2001:1::1/64 R1(config-if)#no sh R1(config-if)#exit R1(config)#ipv6 local …

【自学Java】Java三目运算符

Java三目运算符 Java三目运算符教程 在 Java 语言 中&#xff0c;有一种语言可以等价于使用 if…else 进行变量的赋值操作。它就是三目运算符。 Java语言三目运算符详解 语法 Object result condition?value:value2;参数 参数描述condition条件判断符&#xff0c;一般运…

PointNet++详解(二):网络结构解析

如有错误&#xff0c;恳请指出。 在之前对PointNet与PointNet网络进行了介绍&#xff0c;接下来是对其代码的解析。 1. 论文阅读笔记 | 三维目标检测——PointNet 2. 论文阅读笔记 | 三维目标检测——PointNet 参考的github项目为&#xff1a;https://github.com/yanx27/Poi…

window10安装minio

1、首先第一步我们先下载minio 可以从官网上下不同的版本 下载地址&#xff1a;MinIO | Code and downloads to create high performance object storage 2、启动minio 切记不要双击minio&#xff0c; 1、把下载好的minio.exe放到F(我这里放到F里了)&#xff0c;在地址栏里输…