本博文向大家介紹了插入排序的三種實現:直接插入排序,二分查找插入排序,希爾排序。詳細分析的其實現過程、時間復雜度和空間復雜度、穩定性以及優化改進策略。最后簡單的做了下性能測試。
直接插入排序
(一)概念及實現
直接插入排序的原理:先將原序列分為有序區和無序區,然后再經過比較和后移操作將無序區元素插入到有序區中。
具體如下(實現為升序):
設數組為a[0…n]。
1. 將原序列分成有序區和無序區。a[0…i-1]為有序區,a[i…n] 為無序區。(i從1開始)
2. 從無序區中取出第一個元素,即a[i],在有序區序列中從后向前掃描。
3. 如果有序元素大於a[i],將有序元素后移到下一位置。
4. 重復步驟3,直到找到小於或者等於a[i]的有序元素,將a[i]插入到該有序元素的下一位置中。
5. 重復步驟2~4,直到無序區元素為0。
實現代碼:
public static void Sort<T>(IList<T> arr) where T : IComparable<T> { if (arr == null) throw new ArgumentNullException("arr"); int length = arr.Count(); if (length > 1) { int i, j, k; // 將arr分成有序區和無序區,初始有序區有一個元素 // 0-(i-1) 為有序區;i-(length-1)為無序區 (i從1開始) for (i = 1; i < length; i++) { T temp = arr[i]; // 邊找位置邊后移元素 for (j = i - 1; j >= 0 && arr[j].CompareTo(temp) > 0; j--) arr[j + 1] = arr[j]; // 如果已排序的元素大於新元素,將該元素移到下一位置 // 將 arr[i] 放到正確位置上 arr[j + 1] = temp; } } }
示例:
89,-7,999,-89,7,0,-888,7,-7
排序的過程:
[89] [-7 999 -89 7 0 -888 7 -7]
[-7 89] [999 -89 7 0 -888 7 -7]
[-7 89 999] [-89 7 0 -888 7 -7]
……
……
[-888 -89 -7 -7 0 7 7 89 999] []
(二)算法復雜度
1. 時間復雜度:O(n^2)
直接插入排序耗時的操作有:比較+后移賦值。時間復雜度如下:
1) 最好情況:序列是升序排列,在這種情況下,需要進行的比較操作需(n-1)次。后移賦值操作為0次。即O(n)
2) 最壞情況:序列是降序排列,那么此時需要進行的比較共有n(n-1)/2次。后移賦值操作是比較操作的次數加上 (n-1)次。即O(n^2)
3) 漸進時間復雜度(平均時間復雜度):O(n^2)
2. 空間復雜度:O(1)
從實現原理可知,直接插入排序是在原輸入數組上進行后移賦值操作的(稱“就地排序”),所需開辟的輔助空間跟輸入數組規模無關,所以空間復雜度為:O(1)
(三)穩定性
直接插入排序是穩定的,不會改變相同元素的相對順序。
(四)優化改進
1. 二分查找插入排序:因為在一個有序區中查找一個插入位置,所以可使用二分查找,減少元素比較次數提高效率。
2. 希爾排序:如果序列本來就是升序或部分元素升序,那么比較+后移賦值操作次數就會減少。希爾排序正是通過分組的辦法讓部分元素升序再進行整個序列排序。(原因是,當增量值很大時數據項每一趟排序需要的個數很少,但數據項的距離很長。當增量值減小時每一趟需要和動的數據增多,此時已經接近於它們排序后的最終位置。)
下面來分別介紹:二分查找插入排序和希爾排序
二分查找插入排序
(一)概念及實現
二分查找插入排序的原理:是直接插入排序的一個變種,區別是:在有序區中查找新元素插入位置時,為了減少元素比較次數提高效率,采用二分查找算法進行插入位置的確定。
具體如下(實現為升序):
設數組為a[0…n]。
1. 將原序列分成有序區和無序區。a[0…i-1]為有序區,a[i…n] 為無序區。(i從1開始)
2. 從無序區中取出第一個元素,即a[i],使用二分查找算法在有序區中查找要插入的位置索引j。
3. 將a[j]到a[i-1]的元素后移,並將a[i]賦值給a[j]。
4. 重復步驟2~3,直到無序區元素為0。
實現代碼:
/// <summary> /// 二分查找插入排序 /// </summary> public static void BinarySort<T>(IList<T> arr) where T : IComparable<T> { if (arr == null) throw new ArgumentNullException("arr"); int length = arr.Count(); if (length > 1) { int i, j, k; // 將arr分成有序區和無序區,初始有序區有一個元素 // 0-(i-1) 為有序區;i-(length-1)為無序區 for (i = 1; i < length; i++) { // 二分查找在有序區尋找插入的位置 int index = BinarySearchIndex<T>(arr, i - 1, arr[i]); if (i != index) { T temp = arr[i]; // 后移元素,騰出arr[index]位置 for (j = i - 1; j >= index; j--) arr[j + 1] = arr[j]; // 將 arr[i] 放到正確位置上 arr[index] = temp; } } } } /// <summary> /// 二分查找要插入的位置得Index /// </summary> /// <param name="arr">數組</param> /// <param name="maxIndex">有序區最大索引</param> /// <param name="data">待插入值</param> /// <returns>插入的位置的Index</returns> private static int BinarySearchIndex<T>(IList<T> arr, int maxIndex, T data) where T : IComparable<T> { int iBegin = 0; int iEnd = maxIndex; int middle = -1; int insertIndex = -1; while (iBegin <= iEnd) { middle = (iBegin + iEnd) / 2; if (arr[middle].CompareTo(data) > 0) { iEnd = middle - 1; } else { // 如果是相同元素,也是插入在后面的位置 iBegin = middle + 1; } } return iBegin; }
示例:
89,-7,999,-89,7,0,-888,7,-7
排序的過程:
[89] [-7 999 -89 7 0 -888 7 -7]
[-7 89] [999 -89 7 0 -888 7 -7]
[-7 89 999] [-89 7 0 -888 7 -7]
……
……
[-888 -89 -7 -7 0 7 7 89 999] []
(二)算法復雜度
1. 時間復雜度:O(n^2)
二分查找插入位置,因為不是查找相等值,而是基於比較查插入合適的位置,所以必須查到最后一個元素才知道插入位置。
二分查找最壞時間復雜度:當2^X>=n時,查詢結束,所以查詢的次數就為x,而x等於log2n(以2為底,n的對數)。即O(log2n)
所以,二分查找排序比較次數為:x=log2n
二分查找插入排序耗時的操作有:比較 + 后移賦值。時間復雜度如下:
1) 最好情況:查找的位置是有序區的最后一位后面一位,則無須進行后移賦值操作,其比較次數為:log2n 。即O(log2n)
2) 最壞情況:查找的位置是有序區的第一個位置,則需要的比較次數為:log2n,需要的賦值操作次數為n(n-1)/2加上 (n-1) 次。即O(n^2)
3) 漸進時間復雜度(平均時間復雜度):O(n^2)
2. 空間復雜度:O(1)
從實現原理可知,二分查找插入排序是在原輸入數組上進行后移賦值操作的(稱“就地排序”),所需開辟的輔助空間跟輸入數組規模無關,所以空間復雜度為:O(1)
(三)穩定性
二分查找排序是穩定的,不會改變相同元素的相對順序。
希爾排序
(一)概念及實現
思想:分治策略
希爾排序是一種分組直接插入排序方法,其原理是:先將整個序列分割成若干小的子序列,再分別對子序列進行直接插入排序,使得原來序列成為基本有序。這樣通過對較小的序列進行插入排序,然后對基本有序的數列進行插入排序,能夠提高插入排序算法的效率。
具體如下(實現為升序):
1. 先取一個小於n的整數d1作為第一個增量,將所有距離為d1的倍數的記錄放在同一個組中,把無序數組分割為若干個子序列。
2. 在各子序列內進行直接插入排序。
3. 然后取第二個增量d2<d1,重復步驟1~2,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有記錄放在同一組中進行直接插入排序為止。
實現代碼:
/// <summary> /// 希爾排序 /// </summary> public static void ShellSort<T>(IList<T> arr) where T : IComparable<T> { if (arr == null) throw new ArgumentNullException("arr"); int length = arr.Count(); // 間隔增量,所有距離為space的倍數的記錄放在同一個組中 int space = length / 2; if (length > 1) { while (space >= 1) { ShellInsert(arr, length, space); // 每次增量為原先的1/2 space = space / 2; } } } /// <summary> /// 希爾子序列排序 /// </summary> /// <param name="arr">待排序數組</param> /// <param name="length">待排序數組長度</param> /// <param name="space">間隔增量</param> private static void ShellInsert<T>(IList<T> arr, int length, int space) where T : IComparable<T> { int i, j, k; // 將arr子序列分成有序區和無序區,初始有序區有一個元素 // 0-(i-1) 為有序區;i-(length-1)為無序區 for (i = space; i < length; i++) { T temp = arr[i]; // 邊找位置邊后移元素 for (j = i - space; j >= 0 && arr[j].CompareTo(temp) > 0; ) { // 如果已排序的元素大於新元素,將該元素移到下一位置 arr[j + space] = arr[j]; j = j - space; } // 將 arr[i] 放到正確位置上 arr[j + space] = temp; } }
示例:
89,-7,999,-89,7,0,-888,7,-7
排序的過程:(顏色相同為一個組)
增量為4: 89 -7 999 -89 7 0 -888 7 -7
排序后: -7 -7 -888 -89 7 0 999 7 89
增量為2: -7 -7 -888 -89 7 0 999 7 89
排序后: -888 -89 -7 -7 7 0 89 7 999
增量為1: -888 -89 -7 -7 7 0 89 7 999
排序后: -888 -89 -7 -7 0 7 7 89 999
(二)算法復雜度
1. 時間復雜度: O(nlog2n)
希爾排序耗時的操作有:比較 + 后移賦值。時間復雜度如下:
1) 最好情況:序列是升序排列,在這種情況下,需要進行的比較操作需(n-1)次。后移賦值操作為0次。即O(n)
2) 最壞情況:O(nlog2n)。
3) 漸進時間復雜度(平均時間復雜度):O(nlog2n)
增量選取:希爾排序的時間復雜度與增量的選取有關,但是現今仍然沒有人能找出希爾排序的精確下界。一般的選擇原則是:取上一個增量的一半作為此次序列的划分增量。首次選擇序列長度的一半為增量。(因此也叫縮小增量排序)
平均時間復雜度:O(nlog2n),希爾排序在最壞的情況下和平均情況下執行效率相差不是很多,與此同時快速排序(O(log2n))在最壞的情況下執行的效率會非常差。專家們提倡,幾乎任何排序工作在開始時都可以用希爾排序,若在實際使用中證明它不夠快,再改成快速排序這樣更高級的排序算法.
2. 空間復雜度:O(1)
從實現原理可知,希爾排序是在原輸入數組上進行后移賦值操作的(稱“就地排序”),所需開辟的輔助空間跟輸入數組規模無關,所以空間復雜度為:O(1)
(三)穩定性
希爾排序是不穩定的。因為在進行分組時,相同元素可能分到不同組中,改變相同元素的相對順序。
(四)優化改進
根據實際運行情況,我們也可以將希爾排序中查找插入位置部分的代碼替換為二分查找方式。
性能測試
測試步驟:
1. 隨機生成10個測試數組。
2. 每個數組中包含5000個元素。
3. 對這個數組集合進行本博文中介紹的三種排序。
4. 重復執行1~3步驟。執行20次。
5. 部分順序測試用例:順序率5%。
共測試 10*20 次,長度為5000的數組排序
參數說明:
(Time Elapsed:所耗時間。CPU Cycles:CPU時鍾周期。Gen0+Gen1+Gen2:垃圾回收器的3個代各自的回收次數)
更加詳細的測試報告以及整個源代碼,會在寫完基礎排序算法后,寫一篇總結性博文分享。
喜歡這個系列的小伙伴,還請多多推薦啊…
…