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

 

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:若有描述不當之處,歡迎指正!

相關文章
相關標籤/搜索