排序算法(三) —— 直接插入排序

 

1.     减治法(增量法)

 

  直接插入排序,借鉴了减治法的思想(也有人称之为增量法)。java

  • 减治法:对于一个全局的大问题,和一个更小规模的问题创建递推关系。
  • 增量法:基于一个小规模问题的解,和一个更大规模的问题创建递推关系。

  

  能够发现,不管是减治法仍是增量法,从本质上来说,都是基于一种创建递推关系的思想来减少或扩大问题规模的一种方法。git

  

  很显然,不管是减治法仍是增量法,其核心是如何创建一个大规模问题和一个小规模问题的递推关系。根据应用的场景不一样,主要有如下3种变化形式:github

  • 减去一个常量。(直接插入排序)
  • 减去一个常量因子。(二分查找法)
  • 减去的规模可变。(展转相除法)

 

 

2.     直接插入排序

 

  直接插入排序(straight insertion sort),有时也简称为插入排序(insertion sort),是减治法的一种典型应用。其基本思想以下:算法

  • 对于一个数组A[0,n]的排序问题,假设认为数组在A[0,n-1]排序的问题已经解决了。
  • 考虑A[n]的值,从右向左扫描有序数组A[0,n-1],直到第一个小于等于A[n]的元素,将A[n]插在这个元素的后面。

  很显然,基于增量法的思想在解决这个问题上拥有更高的效率。数组

 

  直接插入排序对于最坏状况(严格递减的数组),须要比较和移位的次数为n(n-1)/2;对于最好的状况(严格递增的数组),须要比较的次数是n-1,须要移位的次数是0。固然,对于最好和最坏的研究其实没有太大的意义,由于实际状况下,通常不会出现如此极端的状况。然而,直接插入排序对于基本有序的数组,会体现出良好的性能,这一特性,也给了它进一步优化的可能性。(希尔排序)dom

 

  直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1),同时也是稳定排序。ide

 

  下面用一个具体的场景,直观地体会一下直接插入排序的过程。性能

场景:优化

现有一个无序数组,共7个数:89 45 54 29 90 34 68。spa

使用直接插入排序法,对这个数组进行升序排序。

 

89 45 54 29 90 34 68

45 89 54 29 90 34 68

45 54 89 29 90 34 68

29 45 54 89 90 34 68

29 45 54 89 90 34 68

29 34 45 54 89 90 68

29 34 45 54 68 89 90

 

直接插入排序的 Java 代码实现:

 1     public static void basal(int[] array) {
 2         if (array == null || array.length < 2) {
 3             return;
 4         }
 5         // 从第二项开始
 6         for (int i = 1; i < array.length; i++) {
 7             int cur = array[i];
 8             // cur 落地标识,防止待插入的数最小
 9             boolean flag = false;
10             // 倒序遍历,不断移位
11             for (int j = i - 1; j > -1; j--) {
12                 if (cur < array[j]) {
13                     array[j + 1] = array[j];
14                 } else {
15                     array[j + 1] = cur;
16                     flag = true;
17                     break;
18                 }
19             }
20             if (!flag) {
21                 array[0] = cur;
22             }
23         }
24     }
basal

 

 

3.     优化直接插入排序:设置哨兵位

 

  

  仔细分析直接插入排序的代码,会发现虽然每次都须要将数组向后移位,可是在此以前的判断倒是能够优化的。

  不难发现,每次都是从有序数组的最后一位开始,向前扫描的,这意味着,若是当前值比有序数组的第一位还要小,那就必须比较有序数组的长度n次。这个比较次数,在不影响算法稳定性的状况下,是能够简化的:记录上一次插入的值和位置,与当前插入值比较。若当前值小于上个值,将上个值插入的位置以后的数,所有向后移位,从上个值插入的位置做为比较的起点;反之,仍然从有序数组的最后一位开始比较。

 

设置哨兵位优化直接插入排序的 Java 代码实现:

 1     // 根据上一次的位置,简化下一次定位
 2     public static void optimized_1(int[] array) {
 3         if (array == null || array.length < 2) {
 4             return;
 5         }
 6         // 记录上一个插入值的位置和数值
 7         int checkN = array[0];
 8         int checkI = 0;
 9         // 循环插入
10         for (int i = 1; i < array.length; i++) {
11             int cur = array[i];
12             int start = i - 1;
13             // 根据上一个值,定位开始遍历的位置
14             if (cur < checkN) {
15                 start = checkI;
16                 for (int j = i - 1; j > start - 1; j--) {
17                     array[j + 1] = array[j];
18                 }
19             }
20             // 剩余状况是:checkI 位置的数字,和其下一个坐标位置是相同的
21             // 循环判断+插入
22             boolean flag = false;
23             for (int j = start; j > -1; j--) {
24                 if (cur < array[j]) {
25                     array[j + 1] = array[j];
26                 } else {
27                     array[j + 1] = cur;
28                     checkN = cur;
29                     checkI = j + 1;
30                     flag = true;
31                     break;
32                 }
33             }
34             if (!flag) {
35                 array[0] = cur;
36             }
37         }
38     }
optimized_1

 

 

4.     优化直接插入排序:二分查找法

 

  优化直接插入排序的核心在于:快速定位当前数字待插入的位置。在一个有序数组中查找一个给定的值,最快的方法无疑是二分查找法,对于当前数不在有序数组中的状况,官方的 JDK 源码 Arrays.binarySearch() 方法也给出了定位的方式。固然此方法的入参,须要将有序数组传递进去,这须要不断地组装数组,既消耗空间,也不现实,可是能够借鉴这方法,本身实现相似的功能。

  这种方式有一个致命的缺点,致使虽然效率高出普通的直接插入排序法不少,可是却不被使用。就是这种定位方式找到的位置,最终造成的数组会打破排序算法的稳定性。既然必定会打破稳定性,那么为何不使用更优秀的希尔排序呢?

 

二分查找法优化直接插入排序的 Java 代码实现:

 1     // 利用系统自带的二分查找法,定位插入位置
 2     // 不稳定排序
 3     public static void optimized_2(int[] array) {
 4         if (array == null || array.length < 2) {
 5             return;
 6         }
 7         for (int i = 1; i < array.length; i++) {
 8             int cur = array[i];
 9             int[] sorted = Arrays.copyOf(array, i);
10             int index = Arrays.binarySearch(sorted, cur);
11             if (index < 0) {
12                 index = -(index + 1);
13             }
14             for (int j = i - 1; j > index - 1; j--) {
15                 array[j + 1] = array[j];
16             }
17             array[index] = cur;
18         }
19     }
optimized_2
 1     // 本身实现二分查找
 2     // 不稳定排序
 3     public static void optimized_3(int[] array) {
 4         if (array == null || array.length < 2) {
 5             return;
 6         }
 7         for (int i = 1; i < array.length; i++) {
 8             int cur = array[i];
 9             // 二分查找的高位和低位
10             int low = 0, high = i - 1;
11             // 待插入的索引位置
12             int index = binarySearch(array, low, high, cur);
13             for (int j = i - 1; j > index - 1; j--) {
14                 array[j + 1] = array[j];
15             }
16             array[index] = cur;
17         }
18     }
19 
20     // 二分查找,返回待插入的位置
21     private static int binarySearch(int[] array, int low, int high, int cur) {
22         while (low <= high) {
23             int mid = (low + high) >>> 1;
24             int mVal = array[mid];
25             if (mVal < cur) {
26                 low = mid + 1;
27             } else if (mVal > cur) {
28                 high = mid - 1;
29             } else {
30                 return mid;
31             }
32         }
33         // 未查到
34         return low;
35     }
optimized_3

 

 

5.     简单的性能比较

 

  

  最后,经过如下程序,简单地统计一下上述各类方法的运行时间。

 

 1     public static void main(String[] args) {
 2 
 3         final int size = 100000;
 4         // 模拟数组
 5         int[] array = new int[size];
 6         for (int i = 0; i < array.length; i++) {
 7             array[i] = new Random().nextInt(size) + 1;
 8         }
 9         
10         // 时间输出:纳秒
11         long s1 = System.nanoTime();
12         StraightInsertion.basal(array);
13         long e1 = System.nanoTime();
14         System.out.println(e1 - s1);
15     }
test

 

执行结果:

 

结论以下:

  • 在某些特定场景下,因为入参的条件不一样,不能执着于 JDK 给的现有方法,自定义的实现效率,可能高于源码的效率。
  • 对于小规模的数组,优化的结果和预想的向左,效率比不上最初的方法。缘由在于自己只是对于判断的优化,而不是执行次数的优化。在每次循环中,加上更多的计算去优化这个判断,在小数组上对于整个排序的效率,反而是一种伤害。
  • 大规模数组,二分查找优化效率明显。

 

 

相关连接:

https://github.com/Gerrard-Feng/Algorithm/blob/master/Algorithm/src/com/gerrard/sort/StraightInsertion.java

 

PS:若有描述不当之处,欢迎指正!

相关文章
相关标签/搜索