直接插入排序,借鑑了減治法的思想(也有人稱之爲增量法)。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:若有描述不當之處,歡迎指正!