排序算法-插入排序

文章目录

  • 插入排序的基本思想
  • 直接插入排序
    • 直接插入排序的思想
    • 直接插入排序的实现
    • 直接插入排序的分析
  • 希尔排序
    • 希尔排序的思想
    • 希尔排序的实现
    • 希尔排序的分析

插入排序的基本思想

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
就类似于我们在玩扑克牌时把扑克牌从小到大排序的过程,从我们拿到牌以后,每摸一张牌,就把摸到的牌插入到我们之前排好的牌中,当我们摸完牌之后牌就是有序的。
插入排序又分为两种,直接插入排序与希尔排序,下面我们来学习以下他俩的思想与实现。

直接插入排序

直接插入排序的思想

假设我们先不是排序一个数组,而是我们现在就有一个有序的数组,如果我们现在想要向这个数组里插入一个值,插入完成后任然保证数组是有序的,就像我们玩扑克摸牌一样,那我们就可以拿这个要插入的值与我们数组里的值挨个进行比较,然后找到它合适的位置插入它,而直接插入排序就是使用了这个思想,如果我们想要排序一个无序数组,我们可以从数组的头部开始,我们先拿到数组的第一个元素,然后我们认为它是有序的,我们再把这个元素后面的哪个元素插入到之前有序的部分中,这样我们的数组就有两个元素都是有序的了,我们再让第三个元素插入到前面的有序部分中,以此类推,直到每一个元素都被插入到有序数列中之后,那么这整个数组就有序了。
下面是直接插入排序的一个流程图,大家可以看看感受一下它的过程。
排序算法-插入排序_第1张图片

了解了直接插入排序的思想,下面我们来实现一下这个代码。

直接插入排序的实现

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

1.我们可以先写一次排序的代码,假设我们现在要在一个有序数组里面插入一个值,那么我们要知道这个有序数组最后一个元素的下标是多少,然后要找到这样元素后面的元素是什么,因为如果我们要在前面的部分插入它,拿我们需要把插入位置后面的元素向后移动一位,以腾出该元素插入的空间,并且会覆盖掉这个元素,所以我们定义一个变量来保存它。
2.现在我们就要拿着刚刚保存的值从最后一个有序数组的位置向前比较了,可以用一个while循环,在比较的同时,我们就可以把前面的元素向后挪了,这样如果我们发现了有比插入值小的元素时,直接插入到这个位置即可,那么循环的判断条件就可以是我们是否访问到数组的第一个元素,如果前面的内容大于比较值,就向后挪。
3.当我们跳出循环时,一般会有两种情况,第一种是前面部分有一个值不大于比较值时,也就是前面的值小于或者等于比较值,那么我们就可以把我们的比价值插入到被我们比较的元素的后面,这样无论是相等还是小于,数组都是有序的。第二种就是循环的判断条件不符合而跳出循环,即end<0,那就代表我们已经把数组前面的元素都已经挨个比较了,但是任然没有找到比比较值小的元素,那就说明比较值就是前面元素中最小的哪一个,这时我们把比较值插入到数组的第一个元素的位置即可。这两种情况都需要对比较值进行插入,那我们就把插入写在while外面,a[end+1]=tmp,这样插入在end>=0时大家可能比较好理解,但是当end<0时,也就是数组头,这时其实end=-1,这样end+1就等于0,也刚好是第一个元素的下标,所以这样写是没有问题的。
4.写完一次比较之后,我们就可以在外面套上一个循环,让数组从第一个元素开始,一直比较到最后一个元素,可以用一个变量i来控制有序部分的最后一个元素的下标,这里要注意i要小于n-1而不是n,因为我们不能让最后一个元素作为有序部分的最后一个元素,否则就会造成数组越界访问。

以上就是直接插入排序的实现
下面我们来分析一下这个算法

直接插入排序的分析

空间复杂度
因为是在数组上直接进行操作,所以空间复杂度为O(1)。

时间复杂度
假设我们要用直接插入排序排以下两个数组
(1)1 2 3 4 5 6
(2)6 5 4 3 2 1
在对数组(1)进行排序时,我们发现我们每次比较位置都是合适的,不需要挪动数组的元素,我们的排序过程就是遍历一遍这个数组,这时的时间复杂度为O(N)。但是当我们对数组(2)进行排序时,我们发现因为它是一个逆序的数组,所以每一次的排序我们都要把所以元素都挪动一遍,其时间复杂度为O(N2),两者差距非常之大。
这时我们就可以发现这样一个规律,直接插入排序在拍一个接近有序的数组时时间复杂度为O(N),效率比较高,但是在排序逆序或者接近逆序的数组时,效率就会变得非常低。

稳定性
排序的稳定性是指这个排序能否做到在排序后让大小相同的数的相对位置不发生不发生变化,我们在处理相同的元素时,是把比较值插入到相同元素的后面,所以他俩的相对位置没有发生变化,这时一个稳定的排序

希尔排序

在刚刚对直接插入排序的分析中,我们发现它其实是有一定缺陷的,即对只有对接近有序的数组排序是效率较高,为了优化插入排序,就出现了希尔排序这种排序算法,也叫缩小增量排序。

希尔排序的思想

希尔排序的步骤主要分为两步:
1.对数组进行预排序(让数组接近有序)。
2.对预排序后的数组进行插入排序。

那么我们如何对数组进行预排序让它接近有序呢?
首先我们把数组进行分组,假设我们把每相隔gap个距离的元素分为一组,把数组分为gap组。
然后我们以每一个组为单位,对他们进行插入排序,这样我们就可以把每一个组的大的元素排到后面,小的元素排到前面,以实现让数组接近有序的目的。
最后我们再对整个数组进行插入排序即可。

下面我画一张流程图大家来感受以下
排序算法-插入排序_第2张图片
假设我们排列的是一个逆序数组,我们设gap=2,在经过我们的预排之后,我们可以看到数组算是非常接近有序了,那么根据插入排序的原理,那我们再对预排之后的数组进行插入排序那效率就高多了。
这时你可能会有疑问,排列预排之后的数组效率是高了,但是还有预排的过程呢,凭什么预排的时间消耗就一定比直接排小呢,其实我们可以这样理解,再预排时,我们是对每一个组进行单独的插入排序,每一个组之间相差的是gap,结果是把每个组的大小元素移动到相应的方向上,而我们每次移动对数组来说都是以gap步为单位的,当gap=1,其实就相当于插入排序,但是当gap比较大时,就等于我们是以更快的速度把数组中的大小元素移动到了对应的方向上,以此实现加快效率的目的。
在实际排序中,我们会让gap从大到小不断变化,直到gap为1,就实现了排序。

希尔排序的实现

假设我们的设最开始的gap大小为数组元素个数的三分之一

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

1.在我们预排时,我们可以不用一个组一个组的排,而可以从第一个元素开始,遇到一个元素,就把他当作这样组里有序部分最大的元素,然后在组里进行插入排序,那么最后的一个元素就应该是下标为n-gap-1的元素,与他对应的后一个元素就应该是下标为n-1的元素。
2.然后与插入排序类似,只不过这次下标的变化是一次变化gap个位置。
3.我们通过将gap不断变小,实现最终变成插入排序的目的,假设每次gap都变成原来的三分之一,为了防止gap在变小的时候变成0,我们要在gap/3后对它加1,这样保证了最后一次肯定是gap=1的插入排序。

以上就是希尔排序实现

希尔排序的分析

空间复杂度
希尔排序算是对插入排序的一个优化它的空间复杂度也是O(1),
时间复杂度
但是希尔排序的时间复杂度就不好计算了,我们以我们刚刚实现的代码为例,我们假设我们有N个元素,那么我们一共就进行了log₃N次预排序,而我们又无法确定每次预排的时间复杂度为多少,所以有人就算出了一个希尔排序时间复杂度的平均值,说是O(N1.3),不知道是怎么算出来的,但是可以肯定的是,希尔排序的时间复杂度肯定与gap有关,我们如何在预排的过程中减小gap的值与gap初始值的大小都会影响到排序的效率,也就是说我们可以根据要排序的数组来调试gap的减小幅度,以增加希尔排序的效率。
稳定性
肯定是不稳定的,因为相同的元素如果不在同一个组,那么我们无法保证他们的相对顺序,所以希尔排序做不到稳定。

你可能感兴趣的:(c语言,数据结构,排序算法,算法)