直接插入排序和希尔排序实现及分析

直接插入排序

代码实现(升序)

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)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}
int main()
{
	int arr[] = { 1,0,2,9,3,8,4,7,5,6 };
	InsertSort(arr, sizeof(arr) / sizeof(arr[0]));
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << ' ';
	}
	return 0;
}

结果

在这里插入图片描述

分析

原理

每次将一个待排序的数据按照大小插入到前面已经排好序的适当位置,直到全部数据插入完成为止。

分析思路

在这里插入图片描述
直接插入排序很像我们玩扑克理牌,我们可以模拟一下这个过程,只要看懂这个日常生活中经常发生的过程,直接插入对你来说就轻而易举了(你可以拿出一张纸跟着下面这五步在上面画一画):
1、我们从牌堆中抽出一个10,我们拿在手上,10本身就是有序的,因为它现在即是最大值也是最小值。现在手上:10
2、我们又从牌堆中抽出一个J,我们把它和10比较了一下,比10大,放在了10的后面。现在手上:10 J
3、我们又从牌堆中抽出一个K,我们把它和J比了一下,比J大,放在了J的后面。现在手上:10 J K
4、我们又从牌堆中抽出一个Q,我们把它和K比了一下,比K小,把K向后挪一个位置,先不把Q插到原来K的位置,因为我们不确定前面的数与Q的大小关系。现在和J比较,发现比J大,现在查到J后面(也就是K原来的位置)。现在手上:10 J Q K
5、我们又从牌堆中抽出一个9,我们把它和K比了一下,比K小,把K向后挪一个位置;我们把它和Q比了一下,比Q小,把Q向后挪一个位置;我们把它和J比了一下,比J小,把J向后挪一个位置;我们把它和10比一下,比10小,把10向后挪一个位置;现在应该让9与前面的元素相比,然而前面没有元素了,于是将9放到了10原来的地方(也就是第一个元素的位置)。现在手上:9 10 J Q K

通过上面的模拟过程,其实我们已经将直接插入的思想以及能遇到的情况阐述完毕了。
我们把插入10的第一步先忽略掉,我们可以想一想,其实,我们就进行了4个回合,并且在每次抽取下一张牌之前,我们面对的都是一个有序的数组:你可以回想一下插入J之前:10;插入K之前:10 J;
我们再想想,这几个回合是怎么结束的:
Ⅰ前面的元素小于自己;
Ⅱ前面没有元素了;

分析代码

Ⅰ分析n-1

外层的for循环次数模拟进行的回合数,进行了n-1次(也就是数组元素数-1次),后面我们发现,i的值赋给了end,并且存在a[end+1];
第一:我们回顾一下上面的抽牌模拟,5张牌,进行了4个真正的回合,得知应该进行数组元素数-1回合;
第二:有a[end+1]存在,发现最多也就进行数组元素数-1次,访问到a[n-1],因为最起码你要保证访问数组时要合法。

Ⅱ分析end的作用

我们先直观的画面标记一下end的位置(就抽出9插入时的回合):(tmp为9)(黄色高亮位置为end的位置)
10 J Q K ->
10 J Q _ K ->
10 J _ Q K ->
10 _ J Q K ->
_ _ 10 J Q K ->
_ 9 10 J Q K
end在一个回合中有两个作用:
1、在一个回合开始时为一个将要把数据插入到的有序数组的界限;
2、通知一直标记着将与tmp比较的元素;

Ⅲ分析break

break是为了解决遇到两个结束条件中的第一个:遇到前面元素小于tmp;

Ⅳ分析a[end + 1] = tmp语句

a[end + 1] = tmp面对:
1、break使得while结束的情况;
break使while结束说明:前一个元素小于tmp;
tmp覆盖到end+1的位置上;
2、end<0使得while结束的情况;
end<0使while结束说明:前面不存在元素了;
end现在等于-1;所以end+1就是第一个元素的位置,将tmp覆盖到这个位置上。

分析时间复杂度

我们先看看什么时候,直接插入算法的时间复杂度最小?已经有序的时候,是O(N),
我们数组越接近有序的时候,时间复杂度越接近O(N);
咱们讨论的使该算法最大的时间复杂度,请问是什么时候?(提个醒我们要把数组排至升序)对!就是当数组为降序时,此时时间复杂度最大;
假如要排这样的数组:
5 4 3 2 1
第一回合:进行1次;第二回合:进行2次;第三回合:进行3次;第四回合:进行4次。
最终我们不难得出:时间复杂度:O(N^2);而面对这样对直接插入最复杂的情况:科学家希尔想到了一个算法:希尔算法来解决这个问题。

希尔排序

代码实现(升序)

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;
		}
	}
}
int main()
{
	int arr[] = { 1,0,2,9,3,8,4,7,5,6 };
	ShellSort(arr, sizeof(arr) / sizeof(arr[0]));
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		cout << arr[i] << ' ';
	}
	return 0;
}

结果

在这里插入图片描述

分析

原理

希尔排序是插入排序的优化,引入了步长的概念。

分析思路

引入步长后,进行预排序,预排序的目的是为了能将相对大的数尽快地移动到后面,而相对小的数尽快地移动到前面。
前面说希尔排序可以很好地解决逆序的问题;
举个例子:6 5 4 3 2 1
我们先将间隙gap设置为3;那么能分出3个组(6,3)、(5、2)、(4、1);
分别对这三个组进行直接插入排序,结果为:3 2 1 6 5 4。
接下来我们再将间隙gap设置为1;分出一组:(3、2、1、6、5、4);接下来进行的行为为排序,而不叫预排序。
我们第一次将gap设置为3,不是随机的,我们先想想:
间隙越大,数字移动更快,但是相对小的间隙排序较无序一些。
希尔排序就运用的分治的思想,我们将一个大问题化成了一个个小的问题,一个个gap进行的回合对应着一个个小的问题。

分析代码

分析gap

这里我们只分析gap,因为其他的都是直接插入的思想。
代码中的出现了五种形式,三种类别:

第一类(int gap = n; gap > 1; gap /= 2;)

我们希望刚开始的 gap 足够大,随后逐渐变小,直到 gap 等于 1 为止,之前的大于 1 的 gap 都是预排序,而当 gap 等于 1 时,进行地都是预排序了,有人问了,那为什么是 gap > 1 并且 gap 总在除以 2 呢。

第一个问题

为什么是 gap > 1,而不是 gap >= 1 呢?如果 gap 等于 1 可以进入循环,我们看到下面有个 gap /= 2 的操作,那么最后传到下面进行排序是, gap 都是 0 了,而非我们所想到的 1 ;

第二个问题

为什么 gap 总在除以 2 ,而不能是 3 呢?其实除以 3 这样的操作稍做一些修改也是可以考虑的,gap /= 3 + 1;我们这样除以2的原因是整型变量一直除以2,总有到 1 的时候。

第二类(for 循环中控制次数的 n - gap操作)

我们拿刚才分析思路中的例子 6 5 4 3 2 1 ,在第一次回合时,end最后只到了 4 的位置,我们的 i 最后是要赋值给 end 的。

第三类(end 与 gap 的之间,并且还有一个end -= gap)

这里gap就是在这里控制步长,我们可以对比一下直接排序的代码。

分析时间复杂度

面对最坏的情况时,我们在第一回合,都只进行 n/2 次操作,第二回合,在第一回合的基础上也是进行关于 n 的操作,直到 gap = 1 时,数组已经非常非常接近有序了,我们知道当数组接近有序时,时间复杂度是O(N);
我们进行的回合数是不同 gap 出现的次数(gap除以2直至等于1为止),我们可以得知gap有logN种。又因为每回合都进行N次操作,所以希尔排序的时间复杂度:O(N*logN);

  • 9
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FeatherWaves

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值