淺解前端必須掌握的演算法(四):希爾排序

程式猿何大叔發表於2018-06-28

前言

雖然前端面試中很少會考到演算法類的題目,但是你去大廠面試的時候就知道了,對基本演算法的掌握對於從事電腦科學技術的我們來說,還是必不可少的,每天花上 10 分鐘,瞭解一下基本演算法概念以及前端的實現方式。

另外,掌握了一些基本的演算法實現,對於我們日常開發來說,也是如虎添翼,能讓我們的 js 業務邏輯更趨高效和流暢。

演算法介紹

希爾排序,也稱遞減增量排序演算法,是插入排序的一種更高效的改進版本。希爾排序是非穩定排序演算法。——維基百科

希爾排序是 D.L.Shell 於 1959 年提出來的一種排序演算法,在這之前排序演算法的時間複雜度基本都是 O(n²),希爾排序演算法是突破該事件複雜度的第一批演算法之一。

科學家希爾研究出來的這種排序方法,對直接插入排序改進後可以增加效率。

演算法闡釋

上一節我們講到的「直接插入排序」,它的效率在陣列本身就是基本有序以及元素個數較少時,它的效率是很高的。但問題就是,這兩個條件本身就很苛刻。如何讓程式爭取實現這倆條件呢?答案就是講原本有大量元素的陣列進行分組,分隔成若干子陣列,這樣每個子陣列的待排序的元素個數就比較少了,然後在子陣列內分別進行「直接插入排序」,當整個陣列基本有序時,再對全體元素進行一次「直接插入排序」。

所謂基本有序,就是小的元素基本在前面,大的基本在後面,不大不小的基本在中間。要注意像 [2, 1, 3, 6, 4, 7, 5, 8, 9] 這樣的可以稱為基本有序,但 [1, 5, 9, 3, 7, 8, 2, 4, 6] 這樣的就談不上了。

因此我們在分割子陣列時,需要採取跳躍分割的策略:將相距某個增量的記錄組成一個子陣列,這樣才能保證在子陣列內分別進行直接插入排序後的得到的結果是基本有序,而不是區域性有序

希爾排序演算法圖示

舉例說明

這個演算法無論怎麼解釋都會顯得含糊不清,直接來個栗子,就拿上圖來說明。

假設現在有一陣列 arr:[8, 9, 1, 7, 2, 3, 5, 4, 6, 0],我們設定初始化步長為 gap = arr.length/2 = 10/2,即 5。按照我們上面說的「跳躍分割策略」,按增量為 5 分割子陣列,將每列看成是一個子陣列:

// 列1 列2 列3 列4 列5
   8   9   1   7   2
   3   5   4   6   0
複製程式碼

然後對每列進行類直接插入排序,可得:

// 列1 列2 列3 列4 列5
   3   5   1   6   0
   8   9   4   7   2
複製程式碼

則此時原陣列順序應變成:[3, 5, 1, 6, 0, 8, 9, 4, 7, 2],然後再縮小增量,gap = 5/2 = 2,則陣列分割如下:

// 列1 列2
   3   5
   1   6
   0   8
   9   4
   7   2
複製程式碼

繼續對每列進行直接插入排序,可得:

// 列1 列2
   0   2
   1   4
   3   5
   7   6
   9   8
複製程式碼

則此時元素組順序應變成:[0, 2, 1, 4, 3, 5, 7, 6, 9, 8],這就是基本有序了。最後一輪再進行微調即可,所以此時增量應計算得為:gap = 2/2 = 1,則直接對陣列應用直接插入排序即可,最後得到:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
複製程式碼

具體實現

var shell_sort = function(arr){
  var i, j, temp, gap;
  var len = arr.length;

  // 逐步縮小增量
  for (gap=len>>1; gap>=1; gap>>=1) {
    // 類直接插入排序演算法
    for (i=gap; i<len; i++) {
      if (arr[i] < arr[i-gap]) {
        temp = arr[i];
        for (j=i-gap; j>=0 && temp<arr[j]; j-=gap) {
          // 記錄後裔,查詢插入位置
          arr[j+gap] = arr[j];
        }
        // 插入
        arr[j+gap] = temp;
      }
    }
  }

  return arr;
};

shell_sort([8, 9, 1, 7, 2, 3, 5, 4, 6, 0]);
複製程式碼

不曉得大家有沒有觀察到,第一層迴圈裡面的兩層巢狀迴圈演算法,其實就是「直接插入排序」,不同就在於多了一個變數 gap,但其實當 gap === 1 時,那就跟我們上一節學到的演算法,是完全一樣的。

演算法實現總結

通過以上程式碼的剖析,大家可以看到,希爾排序的關鍵不是簡單地按 1 為增量進行分組排序後,再合併整體排序;而是選好一個初始化增量,不斷地遞減增量,每次遞減之間都需要經過一次直接插入排序,使得排序的效率提高。

另外只要最終增量為 1,則任何增量序列都可以工作,因為最終當增量為 1 時,演算法就變為「直接插入排序」,這就保證了資料一定會被排序。

複雜度分析

Donald Shell最初建議步長選擇為 n/2 並且對步長取半直到步長達到1。雖然這樣取可以比 O(n²) 類的演算法(插入排序)更好,但這樣仍然有減少平均時間和最差時間的餘地。——維基百科

參考了一下維基百科及相關文章,獲得如下結論:

  1. 希爾排序原始增量序列為 n/(2^i),也就是:n/2, n/4, ..., 1;最壞情況下時間複雜度為 O(n²)
  2. Hibbard 提出的增量序列為 2^k-1,也就是:1, 3, 7, ..., 2^k-1;最壞情況下時間複雜度為 O(n^(3/2))
  3. Sedgewick 提出的增量序列為已知的最好增量序列,也就是:1, 5, 19, 41, 109, .... ;該項序列的項來自淺解前端必須掌握的演算法(四):希爾排序淺解前端必須掌握的演算法(四):希爾排序

綜上所述,希爾排序演算法的出現,我們終於突破了慢速排序的時代,也即超越了時間複雜度為 O(n²)。後面的幾篇文章,我們還會介紹更為高效的排序演算法。

參考連結

zh.wikipedia.org/wiki/%E5%B8…

faculty.simpson.edu/lydia.sinap…


微信公眾號
覺得本文不錯的話,分享一下給小夥伴吧~

相關文章