引言
感觉1年前写的这篇文章有些不全面,因此今天我想完善一下这篇文章。
插入排序的几大优势
毋庸置疑,Insertion Sort 与其它几个高级排序算法(quicksort, heapsort, merge sort)相比,效率要低得多。但是,它也有自己的几大优势:
- 实现简单
- 对于小数据集很有效
- 在实际应用中,它要比其它的一些 $O(n^2)$ 算法(比如:bubble sort)更有效
- Adaptive – 即它可以利用有序元素的优势
- 稳定的,即相等元素的相对顺序不变
- 原址的
实现插入排序
下图是我从 wikipedia 上找到的,它很清楚地演示了插入排序的过程。
我相信大家看过上图以后,应该很清楚排序地整个过程了吧。其实它的整个排序过程像大家玩扑克抓牌的整个过程,你左手里面的牌都是有序的,当你从桌子上抓新牌的时候,你拿它与你左手上的牌逐个进行比较,然后找到一个合适的位置并插入进去。
下面我用 Java 来实现一下 Insertion Sort.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 1public class InsertionSort {
2
3 public static void main(String[] args) {
4 int[] a = {5, 2, 4, 6, 1, 3};
5 sort(a);
6 System.out.println("haha: " + Arrays.toString(a));
7 }
8
9 private static void sort(int[] arr) {
10 for (int j = 1; j < arr.length; j++) {
11 int key = arr[j];
12 int i = j - 1;
13 while (i >= 0 && key < arr[i]) {
14 arr[i + 1] = arr[i];
15 i--;
16 }
17 arr[i + 1] = key;
18 }
19 }
20}
21
22
接下来,我用循环不变式来证明一下上面算法的正确性。
- Loop Invariant : arr[0 … j – 1] 是有序的
- Initialization : 由于从 j = 1 开始,上面循环不变式的数组中只有1个元素,这肯定是有序的
- Maintenance : for 循环内不断地把 arr[j – 1],arr[j – 2],arr[j – 3] 等向右移动,直到为 arr[j] ,即上述代码中的 key 变量,找到一个合适的位置插入进去。因此在下轮迭代前,即把 j 变量增加1之前,arr[0, j] 是有序的,当下一轮迭代开始时,即 j 变量被加1以后,arr[0, j – 1] 依然保持有序
- Termination : 变量 j 的最终值将等于 arr.length ,由于我们已经说明了上面的循环不变式在每次迭代中都保持不变,因此,arr[0, j – 1] ,即arr[0, arr.length – 1] 最终将是有序的,即整个数组都是有序的
分析时间复杂度
从上面的代码可以看出,如果待排序的数组已经有序了,每次 for 循环的迭代中,都不会去进入 while 循环内执行,因此最好情况的时间复杂度为 $O(n)$.
最坏情况就是如果待排序的数组是逆序的,它的时间复杂度为 $O(n^2)$.
平均时间复杂度也是 $O(n^2)$ , 因此 Insertion Sort 不适合排序大的数组。然而,当它用于很小的数组时,它是最快的算法之一,甚至要比 quicksort 还要快; 实际上,一个实现好的 quicksort ,当要排序的元素个数小于某个阙值,算法会自动切换到 Insertion Sort 来排序; 具体的阙值应该去实验,并且也取决于机器,通常情况下,大约是10左右。
通过这个时间复杂度的分析,我相信大家对我上面所说的 Insertion Sort 具有 Adaptive 的性质,会有一个更深地理解。