【数据结构与算法】:插入排序与希尔排序

Alt

🔥个人主页: Quitecoder
🔥专栏: 数据结构与算法

欢迎大家来到初阶数据结构的最后一小节:排序

目录

  • 1.排序的基本概念与分类
    • 1.1什么是排序的稳定性?
    • 1.2内排序与外排序
      • 内排序
      • 外排序
  • 2.插入排序
    • 2.1实现插入排序
    • 2.3稳定性分析
  • 3.希尔排序
    • 3.1预排序代码实现
    • 3.2希尔排序代码实现
    • 3.3时间复杂度分析
  • 4.clock函数
  • 总结

1.排序的基本概念与分类

排序是一种将一组对象按照某种特定顺序重新排列的过程。在计算机科学中,排序是数据处理中非常基本且重要的操作,它可以帮助人们更有效地理解和分析数据。排序的顺序通常是升序或降序,也可以按照数字、字母、大小或其他标准进行

常见的排序算法有冒泡排序、选择排序、插入排序、快速排序、希尔排序、堆排序等等
在这里插入图片描述

1.1什么是排序的稳定性?

排序的稳定性是指在排序过程中,具有相等键值的元素在排序前后保持相同顺序的特性。简单来说,如果排序前两个相等的元素A和B(A出现在B之前),在排序后A仍然出现在B之前,那么这种排序算法就是稳定的;反之,如果排序后A和B的顺序发生了变化,这种排序算法就是不稳定的。

稳定性在某些情况下很重要,尤其是当排序的键值是复合的,即基于多个字段进行排序时。在这种情况下,保持相等元素的初始顺序可能对保持数据的某种有意义的顺序非常关键。例如,在对一组人按出生日期排序时,如果有两个人出生日期相同,我们可能会希望他们在排序后保持按姓名的顺序,如果使用稳定的排序算法,就可以保证这一点。

1.2内排序与外排序

根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:内排序和外排序

内排序

内排序是指所有需要排序的数据都加载到内存中进行排序的过程。这种排序方式适用于数据量不是非常大,能够一次性装入内存的情况。内排序的优点是速度快,因为内存的访问速度远高于磁盘或其他存储设备。常见的内排序算法包括快速排序、归并排序、堆排序、冒泡排序、选择排序、插入排序等。

外排序

外排序是指当需要排序的数据量非常大,一次性无法全部加载到内存中时使用的排序方法。这种情况下,数据通常存储在磁盘或其他外部存储设备上,排序过程中需要多次在内存和存储设备之间交换数据。外排序的一个典型例子是归并排序的一个变种,它将数据分成多个小块,首先对每个小块进行排序(内排序),然后将这些已排序的小块合并成一个有序的整体。外排序适用于大规模数据处理,但速度通常会比内排序慢

接下来我们来介绍两种排序:直接插入排序与希尔排序

2.插入排序

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

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

在这里插入图片描述
扑克牌是我们都玩过的游戏,拿到这组牌,我们进行理牌,将3和4移动到5的左侧,再将⒉移动到最左侧,顺序就算是理好了。这里的理牌方法,就是直接插入排序法。

2.1实现插入排序

思路

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

首先构造函数

void InsertSort(int *a,int n);

在这里插入图片描述

假设这里有数据1 5 8 6

我们认为在0到end这一段有序,把end+1的数插入数组

  • 这里的end代表已排序序列的最后一个元素的索引。将插入的数据依次往前比较,如果比前面的数字end大,则放在end后面
  • 如果比前面的数字小,则让前面的数字往后移动,则这里用变量tmp存放要插入的值
  • 前面的数字移动后,end减一,继续比较

这里还有一种情况
在这里插入图片描述
这里要插入1,遇到7,向前移动,end–,当移动到最前面的位置时,end减为-1
在这里插入图片描述
只考虑单趟排序,可实现代码如下:

int end;
int tmp = a[end + 1];
while (end >= 0)
{
	if (tmp < a[end])
	{
		a[end + 1] = a[end];
		end--;
	}
	else
		break;
}
a[end+1]=tmp;

这里跳出循环有两种情况

  1. 找到插入位置:当tmp大于等于a[end],这表明tmp应该插入在a[end]的后面,因为在tmp之前的元素(包括a[end]自己)都已经排序好了,并且都是小于等于tmp的。这就是tmp的正确位置,在这种情况下,我们执行break语句跳出循环,并将tmp放置在end + 1的位置
  2. 达到有序序列的起点:当循环保持进行,end值在每次迭代中不断递减,如果tmp小于所有已排序的元素,end可能会变成-1,这意味着tmp比有序序列中所有现有的元素都要小,应该被放在整个序列的最开始位置。

在这两种跳出循环的情况下,我们总是需要执行a[end + 1] = tmp;来将tmp元素放置到正确的位置上。因为无论是找到合适的插入点还是tmp成为新的最小元素,我们都需要将它实际插入到有序序列中,这就是为什么这行代码放在循环之外,确保跳出循环后,我们执行最终的插入动作。

考虑单趟排序之后,我们来进行整个过程:

void InsertSort(int *a,int n)
{
	for (int i = 0; i < n; i++)
	{
		int end=i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
				break;
		}
		a[end + 1] = tmp;
	}
}

通过将数组分为已排序部分和未排序部分来工作。从未排序部分取出的值被放置在已排序部分的正确位置。最初,已排序部分只包含数组的第一个元素。

end最初被设置为当前索引i,并将用于通过已排序部分向后遍历,以找到tmp值的正确插入点。

我们进行代码测试:
在这里插入图片描述

插入排序算法的时间复杂度取决于输入数组中元素的初始排序状态:

  • 最坏情况 :如果数组是完全逆序的,那么每次插入操作都需要将元素移到已排序部分的开头。这就意味着对于第i个元素,可能需要进行i次比较和移动。这种情况下,算法的时间复杂度是O(N2),因为需要进行总计约1 + 2 + 3 + … + (n-1)次比较,这是一个n(n-1)/2的等差数列
  • 最好情况 :这种情况发生在数组已经完全有序时。在这种情况下,每次比较后,很快就会找到插入位置(在已排序元素的末尾),不需要进行额外的移动。因此,最好情况下插入排序的时间复杂度是O(N)因为外层循环只会遍历一次数组,内层循环不会进行任何实际的比较和移动操作。

插入排序的空间复杂度为O(1),因为它是一个原地排序算法,不需要额外的存储空间来排序。

2.3稳定性分析

在插入排序中,每个新元素被"插入"到已经排序的序列中,在找到合适的插入位置之前,它不会交换到任何具有相同值的元素前面。我们来逐步分析插入排序算法来说明其稳定性:

  1. 排序初始时,认为第一个元素自成一个已排序的序列
  2. 从第二个元素开始,取出未排序的下一个元素,在已排序的序列中从后向前扫描
  3. 如果当前扫描到的元素大于新元素(待插入),那么将扫描到的元素向后移动一个位置
  4. 重复步骤3,直到找到一个元素小于或等于新元素的位置,或者序列已经扫描完毕
  5. 将新元素插入到这个位置后面

在步骤4中,插入排序的算法逻辑保证了如果存在相等的元素,新元素(待插入)将被放置在相等元素的后面。因此,原始顺序得以保持,插入排序被认为是稳定的

3.希尔排序

希尔排序是一种基于插入排序的算法,通过引入增量的概念来改进插入排序的性能

希尔排序的基本思想是将原始列表分成多个子列表先对每个子列表进行插入排序,然后逐渐减少子列表的数量,使整个列表趋向于部分有序,**最后当整个列表作为一个子列表进行插入排序时,由于已经部分有序,所以排序效率高。**这个过程中,每次排序的子列表是通过选择不同的“增量”来确定的。

实现思路

  1. 预排序
  2. 直接插入排序

预排序:
根据当前增量,数组被分为若干子序列,这些子序列的元素在原数组中间隔着固定的增量。对每个子序列应用插入排序。

假设当前增量为3:
在这里插入图片描述
首先,增量为3,我们将数组元素分为增量(3)个子序列,每个子序列由原数组中相隔增量位置上的元素组成。所以我们有如下子序列:

  • 子序列1: 9, 6, 3, 0
  • 子序列2: 8, 5, 2
  • 子序列3: 7, 4, 1

然后对每个子序列进行独立的插入排序

  • 子序列1排序后:0, 3, 6, 9
  • 子序列2排序后:2, 5, 8
  • 子序列3排序后:1, 4, 7

现在将排序后的子序列放回原数组中,数组变化为:

在这里插入图片描述
完成了一轮希尔排序,此时整个数组并不完全有序,但是已经比原始的数组更接近有序了。然后减小增量,通常是将原来的增量除以2(如果增量序列选择为原始的版本),现在选择下一个增量为 1(因为 3/2 不为整数,所以直接减到 1,进行最后的常规插入排序)。

现在,整个数组就是一个子序列,进行常规的插入排序。

0 2 1 3 5 4 6 8 7 9 <- 初始
0 2 1 3 5 4 6 8 7 9 <- 第1个元素不动,因为它已经在正确的位置
0 1 2 3 5 4 6 8 7 9 <- 将2和1交换
0 1 2 3 5 4 6 8 7 9 <- 3已经在正确位置
0 1 2 3 5 4 6 8 7 9 <- 第5个元素5不动,因为它左边的元素全部小于5
0 1 2 3 4 5 6 8 7 9 <- 将5和4交换
0 1 2 3 4 5 6 8 7 9 <- 第7个元素6不动,因为它左边的元素全部小于6
0 1 2 3 4 5 6 8 7 9 <- 第8个元素8不动,因为它左边的元素全部小于8
0 1 2 3 4 5 6 7 8 9 <- 将8和7交换
0 1 2 3 4 5 6 7 8 9 <- 第10个元素9不动,因为它左边的元素全部小于9

3.1预排序代码实现

我们现在控制实现单趟排序:

int gap = 3;
int end;
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;

与前面代码不同的是,这里对end所加减的均为gap;

单次插入完成后,我们来控制单个子序列的整个过程,以蓝为例,每实现一次排序,下一次插入的数据为end+gap

int gap = 3;

for (int i = 0; i < n-gap; i += gap)
{
	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;
}

这里for循环的条件为i<n-gap防止数组越界

完成单个子序列的排序后,我们再对整个子序列排序:

int gap = 3;
for (int j = 0; j < gap; j++)
{
	for (int i = 0; i < n - gap; i += gap)
	{
		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;
	}
}

外层循环(for (int j = 0; j < gap; j++))意在对每个以gap为间隔的分组进行遍历。

  • j=0,

这串代码三层循环的逻辑是按照每一组排序完成后再进行下一组排序的,事实上我们可以不需要最外层的循环

代码优化:

int gap = 3;
	
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;
}

这里我们将原先代码中的i += gap修改为i++意味着这次不是按照一组一组进行了,是一次排序完每个组的第二个元素,再进行下一个元素的排序

3.2希尔排序代码实现

我们先对预排序的增量进行分析

  • gap越大,大的值更快调到后面,小的值更快调到前面,越不接近有序
  • gap越小,大的值更慢调到后面,小的值更慢调到前面,越接近有序

当gap为1,就是直接插入排序

所以在实现希尔排序时,给gap固定值是行不通的

因此,gap的值是应该随着n来变化的实现多次预排

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 2;
		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是奇数还是偶数,这里gap最终都会除以到值为1

在这里:

  • gap>1时是预排序,目的让其接近有序
  • gap=1时是直接插入排序,目的让其有序

在gap=1时,已经十分接近有序了

这里gap预排序次数还是有点多,因此我们可以再次进行修改,让gap每次除以3,为了使gap最后能回到1,我们进行加一处理

int gap = n;
while (gap > 1)
{
	gap = gap / 3+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;
	}
}

测试代码
在这里插入图片描述

3.3时间复杂度分析

希尔排序的时间复杂度并不固定,它依赖于所选择的间隔序列(增量序列)。直到今天,已经有多种不同的间隔序列被提出来,每种都有自己的性能特点

不同的参考书中给了不同的解释
在这里插入图片描述

在这里插入图片描述

现代更高效的增量序列可以使希尔排序达到O(N log N)时间复杂度,但是没有任何增量序列可以保证希尔排序在所有情况下都达到O(N log
N)。最低的已知上限为O(N (log N)2)。
总的来说,由于增量序列的不确定性,希尔排序的时间复杂度不容易精确描述,实际的时间复杂度取决于所选用的间隔序列以及待排序数组的初始排列状况。在实际应用中,希尔排序的性能通常介于O(N)和O(N2)之间,对于中等大小的数据集,它可以提供非常不错的速度,尤其是因为它比较简单易于实现,且对于较小的数据集,它一般比O(N
log N)复杂度的算法更快。

4.clock函数

clock() 函数是<time.h>头文件中的一个函数,用来返回程序启动到函数调用时之间的CPU时钟周期数。这个值通常用来帮助衡量程序或程序的某个部分的性能

我们可以用这个函数进一步对比两种排序占用的CPU时间

void TestOP()
{
	    srand(time(0));
		const int N = 100000;
		int* a1 = (int*)malloc(sizeof(int) * N);
		int* a2 = (int*)malloc(sizeof(int) * N);
	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		a2[i] = a1[i];
	}
	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();
	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();
	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	free(a1);
	free(a2);
}

这里让随机生成十万个随机数,分别用希尔排序和直接插入排序来进行排序,测试两种算法的执行时间:
在这里插入图片描述
我们可以发现,希尔排序的执行时间相比于直接插入排序很有优势!

总结

希尔排序和直接插入排序都是改进和优化了简单排序算法的典型例子。直接插入排序以其简洁和对于小规模数据集的高效性而著称,特别适合几乎已经排序好的数据。而希尔排序则通过引入“增量”的概念,允许交换距离较远的元素,从而大幅度提高了对大规模数据集的排序效率。两者都在计算机科学的排序算法领域中占有重要地位,不仅展示了算法设计的基本原则,也为更复杂排序算法的发展奠定了基础。

本节内容到此结束,感谢大家的观看!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

区块链基础知识(上):区块链基本原理、加密哈希、公钥加密

目录 基本原理 加密哈希&#xff1a; 公钥加密&#xff1a; 希望有人向你发送只有你才能打开的加密文档/消息时使用 PKC 希望向其他人发送加密文档/消息并证明它确实由你发送时使用 PKC 使用 PKC 和加密哈希对文档/消息进行数字签名 交易哈希链使用数字签名转让数字资产所…

学生时期学习资源同步-1 第一学期结业考试题1

原创作者&#xff1a;田超凡&#xff08;程序员田宝宝&#xff09; 版权所有&#xff0c;引用请注明原作者&#xff0c;严禁复制转载

程序人生 - 公司的技术总监每天都在干蛤?

来看看我的一个技术总监朋友怎么说的。 0、背景 我带领的这支技术团队&#xff0c;规模已达百人。这个数字一直在增长&#xff0c;我感到无比兴奋。团队中的每个成员都独特而不可或缺&#xff0c;他们分别专注于前端、后端、测试、运维和DBA&#xff0c;还有一部分专注于客户端…

基于51单片机的数控直流可调电源设计[proteus仿真]

181基于51单片机的数控直流可调电源设计[proteus仿真] 电源系统这个题目算是课程设计和毕业设计中常见的题目了&#xff0c;本期是一个基于51单片机的数控直流可调电源设计 需要的源文件和程序的小伙伴可以关注公众号【阿目分享嵌入式】&#xff0c;赞赏任意文章 2&#xffe…

Kotlin/Java中String的equals和==

Kotlin/Java中String的equals和 在Java中&#xff0c;如果定义一个常量String和new出一个String对象&#xff0c;是不同的&#xff1a; String s1 "zhang" String s2 new String("zhang") 因为在Java看来&#xff0c;s1只是一个常量&#xff0c;会放在…

FRM模型十六:期权策略(期权组合)

文章目录 备兑看涨期权&#xff08;Covered Call&#xff09;保护看跌期权&#xff08;protective put&#xff09;牛市价差套利熊市价差套利写在后面 本文所有代码基于windAPI&#xff0c;复现前先下载客户端并注册账号 备兑看涨期权&#xff08;Covered Call&#xff09; 构…

CVE-2022-1310:RegExp[@@replace] missing write barrier lead a UAF

文章目录 环境搭建漏洞分析漏洞利用漏洞触发链RCE原语构造 总结参考 环境搭建 嗯&#xff0c;这里不知道是不是环境搭建的有问题&#xff0c;笔者最后成功的实现了任意地址读写&#xff0c;但是任意读写的存在限制&#xff0c;任意写 wasm 的 RWX 区域时会直接报错&#xff0c…

暗光增强——IAT网络推理测试(详细图文教程)

IAT模型由两个独立的分支组成&#xff0c;局部分支用于像素调整&#xff0c;并输出两个用于加法和乘法的特征图。全局分支用于全局调整并输出颜色矩阵和gamma值&#xff0c;全局分支受DETR启发&#xff0c;网络通过动态查询学习的方式更新颜色矩阵和gamma值。整个模型只有超过9…

设置浏览器显示小于12px以下字体

问题 我们在项目开发过程中有时候会遇到设计师给的小于12px的字体&#xff0c;IE、火狐浏览器、移动端等小于12px的字号大小还是可以正常显示的&#xff0c;但是谷歌浏览器上显示字体最小为12px&#xff0c;css设置font-size&#xff1a;10px&#xff0c;运行代码显示结果仍然…

JAVA基础:数组、重载、数据类型、封装、字符串、静态、继承、重写、多态、代码块、权限、接口、内部类

1 数组 //静态初始化 int[] arr1new int[]{1,2,3,4} //简化形式 int[] arr2{1,2,3,4} //动态初始化 int[] arr3new int[5] 2 方法重载 在同一个类中的多个方法的方法名相同,参数个数不同&#xff0c;参数类型不同&#xff0c;参数类型顺序不同 public class Test1 {public …

KubeSphere 社区双周报|2024.02.29-03.14

KubeSphere 社区双周报主要整理展示新增的贡献者名单和证书、新增的讲师证书以及两周内提交过 commit 的贡献者&#xff0c;并对近期重要的 PR 进行解析&#xff0c;同时还包含了线上/线下活动和布道推广等一系列社区动态。 本次双周报涵盖时间为&#xff1a;2024.02.29-03.14…

多媒体操作流程

&#xff01; 从左至右依次为&#xff1a;话筒、投影遥控器、ppt演讲笔、幕布升降遥控器、无线投屏连接器 主机箱 投影仪 二、操作流程 1、打开主机电源&#xff1a;最下面两台设备的开关打开 2、打开投影仪&#xff1a;用投影遥控器对准投影仪按开机键&#xff08;如无需用到…

SwiftUI的context Menu

SwiftUI的 context Menu 现在来演示一下如何使用 SwiftUI 的 Context Menu 。 代码&#xff1a; import SwiftUIstruct ContextMenuBootCamp: View {State var bgColor: Color .purplevar body: some View {VStack(alignment: .leading, spacing: 10.0) {Image(systemName: …

【开源】SpringBoot框架开发公司货物订单管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 客户管理模块2.2 商品维护模块2.3 供应商管理模块2.4 订单管理模块 三、系统展示四、核心代码4.1 查询供应商信息4.2 新增商品信息4.3 查询客户信息4.4 新增订单信息4.5 添加跟进子订单 五、免责说明 一、摘要 1.1 项目…

力扣刷题日记——L83. 删除排序链表中的重复元素

1. 前言 今天是力扣刷题打卡的第四天&#xff0c;今天带来一道简单题。一开始做了一道中等难度的题&#xff0c;但是很遗憾&#xff0c;没有解出来&#xff0c;但是为了不耽误今天的打卡计划&#xff0c;所以先选一个简单题做了&#xff0c;回头做出来那道题再和大家分享。话不…

一口吃掉Linux基础操作

一般在windows上面想要操作Linux系统就需要装软件搞一个虚拟机&#xff0c;我用的是Ubuntu22&#xff0c;就是Linux的发行版.安装Ubuntu的过程比较复杂&#xff0c;最重要的一点是安装时要断网&#xff0c;否则会很慢。 Ubuntu 配置指南 — 地震“学”科研入门教程 先介绍一个…

安卓通过termux部署ChatGLM

一、安装Termux并进行相关配置 1、安装termux Termux 是一个 Android 终端仿真应用程序&#xff0c;用于在 Android 手机上搭建一个完整的 Linux 环境。 不需要 root 权限 Termux 就可以正常运行。Termux 基本实现 Linux 下的许多基本操作。可以使用 Termux 安装 python&…

logistic回归分析

结局变量&#xff1a;二分类&#xff08;常见&#xff09;或多分类变量研究一个或多个原因变量和结果变量的因果关系 eg&#xff1a;Y必须是分类变量

手写简易操作系统(九)--实现打印函数

前情提要 前面我们已经进入内核程序了&#xff0c;中间穿插了一点特权级的知识&#xff0c;现在我们开始准备一个打印函数 很不幸&#xff0c;还有汇编程序 一、C调用规约 因为涉及到C与汇编的联合编程&#xff0c;我们这里简述一下调用规约&#xff0c;调用规约就是约定参…

【DataWhale学习】用免费GPU线上跑chatGLM项目实践

用免费GPU线上跑chatGLM项目实践 ​ DataWhale组织了一个线上白嫖GPU跑chatGLM与SD的项目活动&#xff0c;我很感兴趣就参加啦。之前就对chatGLM有所耳闻&#xff0c;是去年清华联合发布的开源大语言模型&#xff0c;可以用来打造个人知识库什么的&#xff0c;一直没有尝试。而…