直接插入排序,借鉴了减治法的思想(也有人称之为增量法)。java
能够发现,不管是减治法仍是增量法,从本质上来说,都是基于一种创建递推关系的思想来减少或扩大问题规模的一种方法。git
很显然,不管是减治法仍是增量法,其核心是如何创建一个大规模问题和一个小规模问题的递推关系。根据应用的场景不一样,主要有如下3种变化形式:github
直接插入排序(straight insertion sort),有时也简称为插入排序(insertion sort),是减治法的一种典型应用。其基本思想以下:算法
很显然,基于增量法的思想在解决这个问题上拥有更高的效率。数组
直接插入排序对于最坏状况(严格递减的数组),须要比较和移位的次数为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 }
仔细分析直接插入排序的代码,会发现虽然每次都须要将数组向后移位,可是在此以前的判断倒是能够优化的。
不难发现,每次都是从有序数组的最后一位开始,向前扫描的,这意味着,若是当前值比有序数组的第一位还要小,那就必须比较有序数组的长度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 }
优化直接插入排序的核心在于:快速定位当前数字待插入的位置。在一个有序数组中查找一个给定的值,最快的方法无疑是二分查找法,对于当前数不在有序数组中的状况,官方的 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 }
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 }
最后,经过如下程序,简单地统计一下上述各类方法的运行时间。
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 }
执行结果:
结论以下:
相关连接:
PS:若有描述不当之处,欢迎指正!