用JavaScript實現插入排序

翻譯:瘋狂的技術宅
https://medium.com/@jimrottin...

本文首發微信公衆號:前端先鋒
歡迎關注,每天都給你推送新鮮的前端技術文章


插入排序的工作原理是選擇當前索引 i 處的元素,並從右向左搜索放置項目的正確位置。

實現插入排序

插入排序是一種非常簡單的算法,最適合大部分已經被排好序的數據。在開始之前,通過可視化演示算法如何運作一個好主意。你可以參考前面的動畫來了解插入排序的工作原理。

算法的基本思想是一次選擇一個元素,然後搜索並插入到正確的位置。由此纔有了這個名字:插入排序。這種操作將會導致數組被分爲兩個部分 —— 已排序部分和未排序的元素。有些人喜歡把它描繪成兩個不同的數組 —— 一個包含所有未排序的元素,而另一個的元素是完全排序的。但是將其描述爲一個數組更符合代碼的工作方式。

先來看看代碼,然後再進行討論。

const insertionSort = (nums) => {
  for (let i = 1; i < nums.length; i++) {
    let j = i - 1
    let tmp = nums[i]
    while (j >= 0 && nums[j] > tmp) {
      nums[j + 1] = nums[j]
      j--
    }
    nums[j+1] = tmp
  }
  return nums
}

在插入排序的代碼中有兩個索引:iji 用來跟蹤外循環並表示正在排序的當前元素。它從 1 開始而不是0,因爲當我們在新排序的數組中只有一個元素時,是沒有什麼可做的。所以要從第二個元素開始,並將它與第一個元素進行比較。第二個索引 ji-1 開始,從右往左迭代,一直到找到放置元素的正確位置。在此過程中,我們將每個元素向後移動一個位置,以便爲要排序的新元素騰出空間。

這就是它的全部過程!如果你只是對實現感興趣,那你就不用再看後面的內容了。但如果你想知道怎樣才能正確的實現這個算法,那麼請繼續往下看!


檢查循環不量變條件

爲了確定算法是否能夠正常工作而不是恰好得出了給定輸入的正確輸出,我們可以建立一組在算法開始時必須爲真的條件,在算法結束時,算法的每一步都處於條件之中。這組條件稱爲循環不變量,並且必須在每次循環迭代後保持爲真。

循環不變量並不是總是相同的東西。它完全取決於算法的實現,是我們作爲算法設計者必須確定的。在例子中,我們每次迭代數組中的一個元素,然後從右向左搜索正確的位置以插入它。這將會導致數組的左半部分(到當前索引爲止)始終是最初在該數組切片中找到的元素的排序排列。換一種說法是

插入排序的循環不變量表示到當前索引的所有元素“A [0..index]”構成在我們開始排序前最初在“A [0..index]”中找到的元素的排列順序。

要檢查這些條件,我們需要一個可以在循環中調用的函數,該函數作爲參數接收:

  1. 新排序的數組
  2. 原始輸入
  3. 當前的索引。

一旦有了這些,就能將數組從 0 開始到當前索引進行切片,並運行我們的檢查。第一個檢查是新數組中的所有元素是否都包含在舊數組中,其次是它們都是有序的。

//用於檢查插入排序循環不變的函數
const checkLoopInvariant = (newArr, originalArr, index) => {
  //need to slice at least 1 element out
  if (index < 1) index = 1

  newArr = newArr.slice(0,index)
  originalArr = originalArr.slice(0, index)

  for (let i=0; i < newArr.length; i++) {

    //check that the original array contains the value
    if (!originalArr.includes(newArr[i])) {
      console.error(`Failed! Original array does not include ${newArr[i]}`)
    }

    //check that the new array is in sorted order
    if (i < newArr.length - 1 && newArr[i] > newArr[i+1]) {
      console.error(`Failed! ${newArr[i]} is not less than ${newArr[i+1]}`)
    }
  }
}

如果在循環之前、期間和之後調用此函數,並且它沒有任何錯誤地通過,就可以確認我們的算法是正常工作的。修改我們的代碼以包含此項檢查,如下所示:

const insertionSort = (nums) => {
  checkLoopInvariant(nums, input, 0)
  for (let i = 1; i < nums.length; i++) {
    ...
    checkLoopInvariant(nums, input, i)
    while (j >= 0 && nums[j] > tmp) {
      ...
    }
    nums[j+1] = tmp
  }
  checkLoopInvariant(nums, input, nums.length)
  return nums
}

注意下圖中在索引爲2之後的數組狀態,它已對3個元素進行了排序。

clipboard.png

如你所見,我們已經處理了3個元素,前3個元素按順序排列。你還可以看到已排序數組的前3個數字與原始輸入中的前3個數字相同,只是順序不同。因此保持了循環不變量。

分析運行時間

我們將要使用插入排序查看的最後一件事是運行時。執行真正的運行時分析需要大量的數學運算,你可以很快找到自己的雜草。如果你對此類分析感興趣,請參閱Cormen的算法導論,第3版。但是,就本文而言,我們只會進行最壞情況的分析。

插入排序的最壞情況是輸入的數組是按逆序排序的。這意味着對於我們需要迭代每個元素,並在已經排序的元素中找到正確的插入點。外部循環表示從 2 到 n 的總次數(其中 n 是輸入的大小),並且每次迭代必須執行 i-1 次操作,因爲它從 i-1 迭代到零。

clipboard.png

這個結論的證明超出了本文的範圍。老實說,我只是將它與最佳情況進行比較,其中元素已經排序,因此每次迭代所需要的時間都是固定的......

clipboard.png

就 big-O 表示法而言,最壞情況是 Ɵ(n²),最好的情況是Ɵ(n)。我們總是採用最壞情況的結果,因此整個算法的複雜度是Ɵ(n²)。


總結

當輸入的數組已經大部分被排好序時,插入排序的效果最佳。一個好的程序應該是將一個新元素插入已經排好序的數據存儲中。即便是你可能永遠不必編寫自己的排序算法,並且其他類型(例如歸併排序和快速排序)更快,但是我認爲用這種方式去分析算法的確很有趣。


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,每天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,每天都給你推送新鮮的前端技術文章


歡迎繼續閱讀本專欄其它高贊文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章