c语言-数据结构-一篇文章带你玩转希尔排序和插入排序


前言

本篇讲解插入排序和希尔排序的时间复杂度以及实现方式


一、插入排序

顾名思义,插入排序,就是将数据挨个插入进行排序的排序方式

插入排序在生活中很常见,比如说我们打扑克牌的时候,摸一张牌插一张,大的在后小的在前,这就是插入排序的升序

也就是说,插入排序,就是在原本有序的数据中插入新的数据,并改变顺序使其成为一组新的有序数据

如下图所示:
在这里插入图片描述
我们以3、44、38、5四个数字来作为讲解示例
在这里插入图片描述

当我们第一次排的时候,可以将{3}视为一组有序的数据,end指向3,将后面的44插入进去,并使其成为一组新的有序数据

我们使用一个变量存储44,当原数据3<44时,说明此时新数据已经有序,无需改变顺序,直接插入即可,新的一组有序数据便变为了**{3,44}**

第二次排时,end指向44,相当于将38插入到{3,44}这组有序数据中

我们使用一个变量存储38,判断得到44>38,于是end+1的位置38替换为44,即{3,44,44},之后end指向3

此时判断,3 < 38,说明此时已经有序,无需继续改变顺序,直接插入,在end+1即原44(第一个44)的位置替换插入38,变为{3,38,44}

第三次排时,end指向44,相当于将5插入到{3,38,44}这组有序数据中

我们使用一个变量存储5,判断得到44>5,于是将end+1的位置5替换为44,即{3,38,44,44},之后end指向38

此时判断38 > 5,于是将原44的位置即end+1的位置替换为38,此时变为{3,38,38,44},之后end指向3

此时判断3 < 5,此时已经有序,于是在end+1的位置即第一个38处直接插入5,变为{3,5,38,44},此时成为一个新的有序数组

代码实现如下:
在这里插入图片描述
我们将for循环去掉,end的值改为0,便是单趟的插入排序(即只排了一次)

插入排序的时间复杂度最坏情况为O(N²),此时代表每一次插入都要走满,要插入最多次,比较一次插入一次,推导方式为等差数列

最好情况为O(N),此时代表原数据完全有序,无需进行插入排序,直接跳出循环

二、希尔排序

希尔排序是一种效率较高的排序方式,它类似于插入排序,但比插入排序多了一步——>预排序

希尔排序分为两步,一步是预排序,一步是最终的插入排序

顾名思义,预排序就是提前对一组数据进行排序,使其具有一定的有序程度,并不要求达到完全有序,只需比原数据有序一些即可

而希尔排序是预排序,则是通过分组排序来进行的,分组依靠gap,即定义一个gap,gap不断变化,每一个gap对应一次排序,一整组数据共分为gap组,一组数据中每个数据相差gap个位置

譬如说,9,8,7,6,5,4,3,2,1,0这一组数据,我们对它进行希尔排序,排升序,如果先进行单趟单组的希尔排序,我们随机定一个gap,例如gap = 3
分组如图所示:在这里插入图片描述
此时我们便将整组数据分割成为三组,分别是{9,6,3,0}和{8,5,2}以及{7,4,1},接着我们对这三组分别进行插入排序,这就叫做预排序

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 (tmp < a[end])
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			else
			{
				break;
			}
		}
		a[end + gap] = tmp;
	}
}

如代码所示,我们进行的是对{9,6,3,0}这一组的排序,end每次跳gap个位置,这就是单趟单组的预排序

for循环走完后,便排序完毕

如果想要整体排序,那么就要改变条件或者多加一层循环

void ShellSort(int* a, int n)
{
	int gap = 3;
	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 (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

上述代码进行了对三组数据的预排序,此时整组数据已经较为有序,后续继续进行预排序或直接进行插入排序即可,这就是单趟的预排序

并且该代码有两种逻辑实现形式,上述代码的逻辑是先预排一组,再预排另外一组

即每次跳gap个位置,和该位置相差gap的位置进行排序

比如,先对{9,6,3,0}一组进行预排,排完后对{8,5,2}一组进行排序,最后对{7,4,1}进行排序

而另一种逻辑实现则是分组,但是不按照分组去排,而是从第一个一个一个排,并且每个数据之间差gap,即每次跳一个位置,但是和该位置相差gap的位置进行排序

代码如下:

void ShellSort(int* a, int n)
{
	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;
	}
}

此时我们进行的是单趟预排序,如果想进行完整的预排序和插入排序,即完整的希尔排序,那么就要对gap进行动态改变,代码如下:

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		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的动态改变在while循环里,每次循环时while除2,在计算机中,大于2的任何数一直除2,总会有等于1的时候,当gap等于1时,便执行的是直接插入排序,此时排序完毕,判断条件gap = 1,循环结束

而gap的动态改变可以任意,比如gap/2、gap/3 + 1这些都可以,除3加1是为了保证gap一定可以取到1,因为2/3等于0,所以要加1

而gap的动态改变,关系着预排序的趟数,当gap /= 2时,需要排log以2为底的N的对数次

gap越大,跳得越快,大的数越快跳到最后面,总操作次数越少,时间复杂度越小

gap越小,跳得越慢,大的数越慢跳到最后面,总操作次数越多,时间复杂度越大

同时,gap的动态改变又关系着整个希尔排序的时间复杂度

我们以第一种实现方式为例
在这里插入图片描述
如图所示,我们暂且只看一趟预排序的时间复杂度,它包含最里层的while循环和外层的for循环,我们固定gap的值,此时相当于直接插入排序,求得时间复杂度暂定为O(N²),推导方式为等差数列

而当我们动态改变gap,即计算外层while循环时,此时希尔排序的时间复杂度明面上仍然为O(N²)

在这里插入图片描述

我们假定n = 10,数据为{9,8,7,6,5,4,3,2,1,0}此时gap = 5,那么共分为五组,而每组中的数据之间相差5个位置

那么当时gap = n,进入循环后,gap /= 2,即gap = n / 2,此时被分为gap组,每组中的数据之间相差gap

此时进入循环,在这里插入图片描述

如图,我们假定每组数恰好有四个

那么当gap = n / 2时,总操作次数为1 * (gap),即1 * n/2 = 0.5n

1代表最坏情况下要排的总次数,即最坏情况下插入的总次数

gap代表组数,每组都走最坏次,那么最后得到第一趟排序时间复杂度就是0.5n,即O(N)

第二次排序时,gap = n / 4,此时每组数据个数为4(n/gap,即n /(n/4)),
那么总操作次数为:(1+2+3)* n / 4,即6 * n/4 = 1.5n

1+2+3代表最坏情况下要排的总次数,即四个数最坏情况下插入的总次数

gap代表组数,每组都走最坏次,那么最后得到第一趟排序时间复杂度明面上就是1.5n,即O(N)

在这之后gap不断减小,排序总次数不断增多,时间复杂度也越来越大

但是在计算的时候我们忽略了一个地方——预排序对于后续排序的影响

我们在之前提到过,希尔排序分为预排序和最后一次的直接插入排序,预排序的功能就是为了让原数据变得相对有序一些,从而减少直接插入排序比较的次数

而我们在执行第一次循环gap=n/2时,已经对原数据进行一趟预排序,因此当gap=n/4时,我们的数据已经变得相对有序一些了,如果我们假定原数据是完全无序,第一趟预排序每次都要是最坏情况的话,那么第二趟预排序进行的时候,不可能每一次都会走到最坏情况,因此第二趟预排序的实际排序总次数是要小于1.5n的,但是我们目前无法计算,因此暂定为1.5n
趋势图如下:
在这里插入图片描述
按正常来说,gap不断减小,排序次数应该不断增多,因为每组的数据越来越多

但是从图中我们可以看到,当gap不断减小时,我们的总操作次数先增大后减小

这就是因为预排序对于后续排序的影响,预排序使得原数据已经变得相对有序,因此后续排序的次数会受到影响,继而减少

我们知道,当gap=1时,这就是最后一次排序,也相当于直接插入排序,如果按照最坏情况来算应该是n²,但是因为受到前面几次预排序的影响,不可能是最坏情况,它已经很接近有序状态了,因此实际上要小于n²

但是这个影响不会突破一个量级,最后算出的应该是未知常系数 * N²甚至可能是n,小于n²但又不会突破这个量级,例如它不会完全到达n这个量级,介于n与n²之间

而我们的时间复杂度可以用排序总次数来计算,即:0.5n + 1.5n(实际小于1.5n)+…+n²(实际小于n²),而前面的n无法影响到最大量级n²

而通过查阅殷老师和严老师的两本数据结构讲解书籍中可以知晓,经过大量实验数据检测后,希尔排序的时间复杂度一般可以认为到达了是在O(N^1.3)到O(N²)之间

总结

本文介绍了插入排序和希尔排序两种排序算法的实现方式及时间复杂度分析。插入排序通过逐个插入元素构建有序序列,时间复杂度为O(N²)最坏情况,O(N)最好情况。希尔排序是插入排序的改进版,通过预排序(分组排序)提高效率,包含多次预排序和最终插入排序两个阶段。预排序通过动态调整gap值(初始为n/2并逐步减半)实现分组,使数据趋于有序。希尔排序的时间复杂度约为O(N^1.3),优于直接插入排序。文中通过图示和代码示例详细说明了两种排序的具体实现过程及性能优化原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值