vlambda博客
学习文章列表

图解直接插入排序和希尔排序

前言

之前我们曾经介绍了选择类排序中的简单选择排序,它的时间复杂度是O(n^2)。这次我们介绍插入类排序中的直接插入排序和希尔排序。

对于直接插入排序,虽然它的时间复杂度也是O(n^2),但是在元素有序或近乎有序的情况下,时间复杂度可以降为O(n),效率比O(nlogn)的算法还要高。

然而对于大规模的乱序数组,使用直接插入排序的效率是非常低的。此时我们需要使用希尔排序,希尔排序在直接插入排序的基础上,弥补了直接插入排序只能比较相邻元素的不足,使得元素可以按照指定步长比较元素,充分发挥了直接插入排序对于小规模有序数组排序的优势。

下面我们分别介绍直接插入排序和希尔排序两类插入排序。

直接插入排序

有如下数组,我们需要对它从小到大排序,利用直接插入排序步骤如下:

1.将第二个元素和第一个元素比较,小于第一个元素的话交换位置,这样第二个元素作为最小的元素排在了最前面,大于的话不交换。2.然后将第三个元素和第二个元素比较,小于第二个元素的话交换位置,然后再和第一个元素比较,小于第一个元素的话再次交换位置,这样第三个元素作为最小的元素排在了最前面,大于的话不交换。3.以此类推,直到最后一个元素插入到合适位置。

下图展示了整个排序的过程:

图解直接插入排序和希尔排序

直接插入排序的代码:

public static void sort(Comparable[] arr) { int n = arr.length; // 0位置不需要比,从1到最后一个位置n-1 for (int i = 1; i <= n - 1; i++) { for( int j = i; j > 0 && arr[j].compareTo(arr[j-1]) < 0 ; j--) { swap(arr, j, j-1); } }}
private static void swap(Object[] arr, int i, int j) { Object t = arr[i]; arr[i] = arr[j]; arr[j] = t;}

优化

优化的思路就是将内循环中每次swap交换操作修改为让较大的元素后移,最后再进行一次交换,这样一来访问数组的次数就减少了(交换需要三行,赋值只需要一行)。

优化步骤如下:

1.每次先保存当前插入的元素。2.将当前保存的元素(第二个元素)和第一个元素比较,小于第一个元素的话,将第一个元素移动到第二个元素位置,然后当前保存的元素移动到第一个元素位置,这样第二个元素作为最小的元素排在了最前面。3.将当前保存的元素(第三个元素)和第二个元素比较,小于第二个元素的话,将第二个元素移动到第三个元素位置,然后当前保存的元素移动到第二个元素位置,接着小于第一个元素的话再次执行以上操作,这样第三个元素作为最小的元素排在了最前面。4.以此类推,直到最后一个元素插入到合适位置。

下图展示了优化思路的过程:

优化的直接插入排序代码:

public static void sort(Comparable[] arr) { int n = arr.length; // 0位置不需要比,从1到最后一个位置n-1 for (int i = 1; i <= n - 1; i++) { // 保存当前插入的元素 Comparable e = arr[i]; int j; for (j = i; j > 0 && arr[j - 1].compareTo(e) > 0; j--) { arr[j] = arr[j - 1]; } arr[j] = e; }}

希尔排序

上面我们介绍了直接插入排序,它对于大规模乱序数组的排序效率比较低,因为只能交换相邻的元素,所以元素只能一点一点地从数组的一端移动到另一端。此外,如果最小的元素在数组的末尾,那么将它插入到正确位置需要移动 N-1 次。

希尔排序的出现,解决了上述问题。它能够交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。

希尔排序的思想是使数组中任意间隔为 h 的元素都是有序的。这样的数组也叫作 h 有序数组。我们不研究 h 是如何得来的,这里直接使用了《算法》书中的 h 步长序列。

h = 3*h+1,根据 h 的取值分别为1、4、13 ...

实际上只需要把直接插入排序代码中移动元素的距离由 1 改为 h 即可。这样,希尔排序就转换为了一个类似于直接插入排序但使用不同增量的过程。

下图展示了希尔排序的过程:

如果你仔细观察,会发现在 h=1 时,相比直接插入排序,比较的次数大大减少了,这是因为希尔排序使得部分子数组有序,而直接插入排序对于近乎有序的数组,效率是非常高的。

希尔排序代码:

public static void sort(Comparable[] arr) { int n = arr.length; // 步长序列: 1, 4, 13... int h = 1; while (h < n / 3) { h = 3 * h + 1; }
while (h >= 1) { for (int i = h; i < n; i++) { // 将 arr[i] 插入到 arr[i-h], arr[i-2*h], arr[i-3*h]... 中 Comparable e = arr[i]; int j; // 优化的插入排序 for (j = i; j >= h && e.compareTo(arr[j - h]) < 0; j -= h) { arr[j] = arr[j - h]; } arr[j] = e; } h /= 3; }}

总结

本文介绍了直接插入排序和希尔排序这两类插入排序,通过比较这两类排序,我们了解了快速排序适用于小规模数组或有序数组,希尔排序适用于中等规模的乱序数组。