1. 程式人生 > 其它 >二分查詢插入排序以及一些思考

二分查詢插入排序以及一些思考

  二分查詢的插入排序是在處理待處理插入元素時,在已排好序的集合中採用二分法查詢到插入的位置,而後進行整體後移,騰出這個插入的位置後,將該元素插入。

  思路並不難理解,但是當我面對這個二分法實現的時候,還是覺得有很多細節不好處理,於是有了以下思路的記錄:

定義i為當前輪到的比較元素,如果i<2,那麼不需要比較,直接返回。
1.確定順序集合的邊界
  從0到i-1
2.確定中點位置
  如果是奇數:從0開始算是偶數個元素,所以無法確定到真正的中位,只能中位最近的兩個元素取一個,(i-1-0)/2滿足條件
  如果是偶數:從0開始算是奇數個元素,(i-1-0)/2就是真正的中位元素,滿足條件
  這個結論對於非0開始的元素也是成立的
  所以,設定開始(include)進行計數的邊界為start,結束(include)計數的邊界為(i-1),那麼((i-1-start)/2)+start就是中點位置元素
3.一般情況
  第一輪比較,0和i-1計算出中點位置為k=(i-1-0)/2,將i位置與k位置進行比較
  如果i位置元素更大,那麼下一輪比較的範圍是k+1,i-1;
  如果i位置元素更小,那麼下一輪比較的範圍是0,k-1;
  如果i位置元素相等,那麼此時就得到了i的插入位置為k+1,原k+1位置到i-1位置全部後移一個位置。
  第二輪比較,計算出中點位置為k1=((i-1-k-1)/2)+k+1,將i位置與k1位置進行比較
  ...
4.邊界情況
  在步驟3中,討論的是正常比較和推進的策略,這裡步驟4討論的是比較的終止條件。
  在步驟3中已經討論了一種插入情況,就是找到了與i元素相等元素的位置時,可以插入;但是,如果整個序列裡沒有相等的情況,我們就需要用邊界去比較得到最終的插入位置。
  計數和偶數序列的最後一次二分判斷情況應該如下圖所示:

  如果剩餘的是1個元素,即邊界[m,n]中,m==n,此時只需要判斷i位置元素與這個元素的大小就能確定插入位置:
  如果小於等於該剩餘元素,那麼插入位置為m;
  否則插入位置為m+1;

  如果剩餘的是2個元素,即邊界[m,n]中,m+1==n,那麼此時需要根據與這兩個元素的比較結果確定插入位置:
  <=左側元素,那麼插入位置為m;
  >=右側元素,那麼插入位置為n+1;

  同時不滿足以上兩點,那就證明這個元素的值介於左右側元素之間,且都不相等,那麼插入位置為m+1;
  如果剩餘的是3個及以上,那麼還可以進行再次的二分判斷邏輯。

 綜合上面的分析,邏輯編寫時,while的判定條件應該就是m和n的關係來確定。先進行m與n的關係判斷,而後內部的迴圈應該是靠不斷地改變邊界來推動比較過程,最終確定插入位置。
 注意,這裡是可以寫成一個遞迴函式的,但是能用while搞定就儘量不要用遞迴,因為遞迴還需要考慮棧深。
 另外,由於查詢到了插入位置後,後續的位置交換也是固定的,此時可以從i-1位開始向前推進,每一位都往後一位覆蓋,一直到插入的位置,不用再進行交換操作。
 以上其實是一種分析二分法的套路,也可以算作一種分析比較複雜情況的分析套路。

=================================================================  

   程式碼經過編寫除錯如下:

public class BinarySearchInsertionSort {
    public static void sort(int[] array) {
        if (array == null || array.length < 2) {
            return;
        }

        int start = 0;
        int end = 0;
        
int insert = 0; for (int i = 0; i < array.length - 1; i++) { //1.找到已排好的序列,找到需要進行插入的元素 //待處理的元素如果已經超過範圍,說明排序已經完成,可以退出迴圈 int process = end + 1; if (process > array.length - 1) { break; } //比較待二分處理的序列,如果序列長度超過2,才進行二分處理 boolean findLocation = false; while (end - start >= 2) { //進行二分處理,找到插入位置 int middle = start + ((end - start) / 2); if (array[process] == array[middle]) { insert = middle + 1; findLocation = true; break; } else if (array[process] > array[middle]) { start = middle + 1; } else { end = middle - 1; } } if (!findLocation) { //如果到這裡,說明本身序列長度不超過2個;或者,在while中沒有找到相等的middle位置 insert = findInsertLocationForLessEqualTwo(array, start, end, insert, process); } //2.找到了插入位置,這裡進行元素的後移 int readyInsertValue = array[process]; for (int j = process; j > insert; j--) { array[j] = array[j - 1]; } array[insert] = readyInsertValue; //3.確定下一個元素二分查詢的start與end位置 start = 0; end = i + 1; } } private static int findInsertLocationForLessEqualTwo(int[] array, int start, int end, int insert, int process) { if (start == end) { if (array[process] <= array[start]) { insert = start; } else { insert = start + 1; } } else if (start + 1 == end) { if (array[process] <= array[start]) { insert = start; } else if (array[process] >= array[end]) { insert = end + 1; } else { insert = start + 1; } } else { throw new RuntimeException("logic error"); } return insert; } public static void main(String[] args) { int[] array = {2, 3, 1, 4, 2, 44, 32, 13, 12, 56, 32, 12, 33, 21, 15, 16, 76, 45, 32, 33, 121, 123, 123, 122}; sort(array); System.out.println("Arrays.toString(array) = " + Arrays.toString(array)); } }

  這裡需要注意的是,二分查詢只是將查詢的效率提高了,但是在一輪插入最壞的情況下,還是會出現n-1次元素賦值操作,所以演算法複雜度沒有根本變化。最優時O(n),最差時O(n^2)。

=================================================================

  在整理這個思路的過程中,我愈發覺得,其實要思考了整個過程,才能形成一些自己的套路。就像熟悉了一個有序集合可以使用二分法,也像單軸快排與雙軸快排對比,會發現雙週快排其實是一個輪次做了更多的事情,這樣的思路可能會讓我們在新場景舊問題的情況下很快形成解決思路,而對於這種本質的把控,可能真的需要我們將整個邏輯在腦子裡不斷地放電影,形成動圖,才有可能實現。