[算法]插入排序和希尔排序

        这里简单的介绍一下插入排序和希尔排序的算法实现,为简单起见,排序为升序且排序的数组是整形数组。

一、插入排序

(一)、算法思路

        把数组里的第一个元素视为有序的,然后取第二个元素与前面的元素作比较,如果该元素小于第一个元素,则把第一个元素往后移,第二个元素往前移,这样,我们数组前两个元素就变得有序了。接着再取第三个元素,将其与前面的元素作比较,如果该元素小于其前面的元素,则将该元素往前移,将被比较的元素往后移,然后再将该元素与前一个元素作比较,如果当该元素大于其前面的元素,那么当前该元素所在的位置就是其正确的顺序,这个时候,数组有序的序列长度增加了一,其结果就是把一个元素插入到前面的已经有序的序列当中去,所以叫做插入排序,以此重复插入排序,数组变得有序。

下面是插入排序的动图:

(二)、算法的实现 

void InsertSort(int* arr, int nums)
{
	for (int i = 1; i < nums; i++)
	{
		int key = arr[i];//存储待插入排序的值

		int j;
		for (j = i; j > 0; j--)
		{
			//依次往前作比较进行移动
			if (key < arr[j - 1])
				arr[j] = arr[j - 1];
			else
				break;
		}

		//此时j的位置就是key插入排序的位置
		arr[j] = key;
	}
}

(三)、时间复杂度和稳定性

        插入排序的时间复杂度为O(n^{2})当待排序的数组为完全或接近有序时,插入排序的时间复杂度为O(n),而当待排序的数组完全或接近逆序时,插入排序的时间复杂度为O(n^{2})

        插入排序是稳定的。

二、希尔排序

        希尔排序(Shell's Sort)插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。

(一)、算法思路

        希尔排序是从插入排序上改进的,先来看看改进了什么。假如有一组数组的序列是逆序的:[10,9,8,7,6,5,4,3,2,1],那么我们的插入排序在每次插入待排序的元素时,都要以一次移动一个数据这样(增量为一)依次移动到数组的开头位置,这样时间复杂度就为O(n)了。而我们的希尔排序一开始以一个增量开始跳跃式的进行插入排序,叫做预排序将数组中较小的元素移动到数组的左边去,将数组中较大的元素移动到数组的右边去,排序一次后,数组左边差不多都是小的元素,数组的右边差不多都是大的元素。再缩小增量进行增量插入排序,跳跃进行插入排序的跳跃元素个数减小,执行该增量的插入排序,以此重复,每次执行完增量插入排序后数组的元素逐渐接近有序,当增量变为一时,执行的是朴素的插入排序,这个时候因为预排序的作用数组的元素已经非常接近有序了,执行完插入排序后,数组变为有序。

下面以缩小增量为除以2进行希尔排序来演示,假设待排序的数组为:

5 7 2 10 6 4 3 1 8 9

增量为5:

以增量为10 / 2 = 5 分组进行增量插入排序
5 7 2 10 6 4 3 1 8 9
对第一组红色元素进行增量插入排序
4 7 2 10 6 5 3 1 8 9
对第二组黄色元素进行增量插入排序
4 3 2 10 6 5 7 1 8 9
对第三组绿色元素进行增量插入排序
4 3 1 10 6 5 7 2 8 9
对第四组蓝色元素进行增量插入排序
4 3 1 8 6 5 7 2 10 9
对第五组紫色元素进行增量插入排序
4 3 1 8 6 5 7 2 10 9

增量为2:

以增量为5 / 2 = 2为增量分组进行插入排序
4 3 1 8 6 5 7 2 10 9
对第一组红色元素进行增量插入排序
1 3 4 8 6 5 7 2 10 9
对第二组黄色元素进行增量插入排序
1 2 4 3 6 5 7 8 10 9

增量为1:

以增量为2 / 2 = 1为增量进行朴素插入排序
1 2 4 3 6 5 7 8 10 9
对第一组红色元素进行增量插入排序
1 2 3 4 5 6 7 8 9 10

        我们可以看到,前两次的预排序使得最后一次的朴素插入排序非常轻松,因为待排序的数组经过预排序后变得十分接近有序了。

(二)、算法实现 

//希尔排序
void SheelSort(int* arr, int nums)
{
	int gap = nums;
	
	while (gap > 1)
	{
		//缩小增量进行插入排序
		gap /= 2;

		//分组进行插入排序
		//有gap组
		for (int i = 0; i < gap; i++)
		{
			//增量为gap的插入排序
			for (int j = i + gap; j < nums; j += gap)
			{
				int key = arr[j]; //存储待增量插入排序的值

				int k;
				//注意这里的结束条件是k >= gap 因为要保证k - gap >= 0
				for (k = j; k >= gap; k -= gap)
				{
					//依次往前作比较进行移动
					if (key < arr[k - gap])
						arr[k] = arr[k - gap];
					else
						break;
				}
				//此时k的位置就是key插入的位置
				arr[k] = key;
			}
		}

	}
}

上面的代码有三个循环嵌套,我们还可以使其变得简单点。

        我们可以在每次进行增量插入排序时,i从0遍历到nums - gap - 1的位置,然后依次将其遍历的位置的以增量为单位的下一个元素key进行增量插入排序

比如上面的:

以增量为5 / 2 = 2为增量分组进行插入排序
4 3 1 8 6 5 7 2 10 9

我们可以这样进行增量插入排序:

i = 0

将key = 1 插入到有序序列 4 中

结果:

 

i = 1

key = 8 插入到有序序列 3 中

结果:

 

 

i = 2

key = 6 插入到有序序列 1 4 中

结果:

i = 3

key = 5 插入到有序序列 3 8 中

结果:

i = 4

key = 7 插入到 有序序列 1 4 6 中

结果不变

 i = 5 

key = 2 插入到有序序列 3 5 8 中

结果:

i = 6

key =10 插入到 有序序列 1 4 6 7 中

结果不变 

i = 7

key = 9 插入到有序序列 2 3 5 8 9 中

结果不变:

1 2 4 3 6 5 7 8 10 9

这就是通过从头遍历实现执行一次预排序的过程。

具体代码如下:

void SheelSort(int* arr, int nums)
{
	int gap = nums;

	while (gap > 1)
	{
		gap /= 2;

		for (int i = 0; i < nums - gap; i++)
		{
			int j;
			int key = arr[i + gap];//存储待插入排序的值
			for (j = i; j >= 0; j -= gap)
			{
				if (key < arr[j])
					arr[j + gap] = arr[j];
				else
					break;
			}
			//此时j + gap的位置为key插入的位置
			arr[j + gap] = key;
		}
	}
}

(三)、时间复杂度和稳定性

        希尔排序的时间复杂度比较复杂,大概为O(n^{1.3-2}),其排序算法不稳定。

        上面的代码中的缩小增量为除以2,其实还可以是其他值,如 3 4 5 .....,只要保证最后一次可以执行增量为1的朴素插入排序即可,如当缩小增量的倍率为3时,gap = gap / 3 + 1。

猜你喜欢

转载自blog.csdn.net/SUICIDEKILL/article/details/140736429