翻譯:瘋狂的技術宅
https://medium.com/@jimrottin...
本文首發微信公衆號:前端先鋒
歡迎關注,天天都給你推送新鮮的前端技術文章javascript
插入排序的工做原理是選擇當前索引 i 處的元素,並從右向左搜索放置項目的正確位置。前端
插入排序是一種很是簡單的算法,最適合大部分已經被排好序的數據。在開始以前,經過可視化演示算法如何運做一個好主意。你能夠參考前面的動畫來了解插入排序的工做原理。java
算法的基本思想是一次選擇一個元素,而後搜索並插入到正確的位置。由此纔有了這個名字:插入排序。這種操做將會致使數組被分爲兩個部分 —— 已排序部分和未排序的元素。有些人喜歡把它描繪成兩個不一樣的數組 —— 一個包含全部未排序的元素,而另外一個的元素是徹底排序的。可是將其描述爲一個數組更符合代碼的工做方式。程序員
先來看看代碼,而後再進行討論。面試
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 }
在插入排序的代碼中有兩個索引:i
和 j
。 i
用來跟蹤外循環並表示正在排序的當前元素。它從 1 開始而不是0,由於當咱們在新排序的數組中只有一個元素時,是沒有什麼可作的。因此要從第二個元素開始,並將它與第一個元素進行比較。第二個索引 j
從 i-1
開始,從右往左迭代,一直到找到放置元素的正確位置。在此過程當中,咱們將每一個元素向後移動一個位置,以便爲要排序的新元素騰出空間。算法
這就是它的所有過程!若是你只是對實現感興趣,那你就不用再看後面的內容了。但若是你想知道怎樣才能正確的實現這個算法,那麼請繼續往下看!segmentfault
爲了肯定算法是否可以正常工做而不是剛好得出了給定輸入的正確輸出,咱們能夠創建一組在算法開始時必須爲真的條件,在算法結束時,算法的每一步都處於條件之中。這組條件稱爲循環不變量,而且必須在每次循環迭代後保持爲真。數組
循環不變量並非老是相同的東西。它徹底取決於算法的實現,是咱們做爲算法設計者必須肯定的。在例子中,咱們每次迭代數組中的一個元素,而後從右向左搜索正確的位置以插入它。這將會致使數組的左半部分(到當前索引爲止)始終是最初在該數組切片中找到的元素的排序排列。換一種說法是微信
插入排序的循環不變量表示到當前索引的全部元素「A [0..index]」構成在咱們開始排序前最初在「A [0..index]」中找到的元素的排列順序。
要檢查這些條件,咱們須要一個能夠在循環中調用的函數,該函數做爲參數接收:多線程
一旦有了這些,就能將數組從 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個元素進行了排序。
如你所見,咱們已經處理了3個元素,前3個元素按順序排列。你還能夠看到已排序數組的前3個數字與原始輸入中的前3個數字相同,只是順序不一樣。所以保持了循環不變量。
咱們將要使用插入排序查看的最後一件事是運行時。執行真正的運行時分析須要大量的數學運算,你能夠很快找到本身的雜草。若是你對此類分析感興趣,請參閱Cormen的算法導論,第3版。可是,就本文而言,咱們只會進行最壞狀況的分析。
插入排序的最壞狀況是輸入的數組是按逆序排序的。這意味着對於咱們須要迭代每一個元素,並在已經排序的元素中找到正確的插入點。外部循環表示從 2 到 n 的總次數(其中 n 是輸入的大小),而且每次迭代必須執行 i-1
次操做,由於它從 i-1
迭代到零。
這個結論的證實超出了本文的範圍。老實說,我只是將它與最佳狀況進行比較,其中元素已經排序,所以每次迭代所須要的時間都是固定的......
就 big-O 表示法而言,最壞狀況是 Ɵ(n²),最好的狀況是Ɵ(n)。咱們老是採用最壞狀況的結果,所以整個算法的複雜度是Ɵ(n²)。
當輸入的數組已經大部分被排好序時,插入排序的效果最佳。一個好的程序應該是將一個新元素插入已經排好序的數據存儲中。即使是你可能永遠沒必要編寫本身的排序算法,而且其餘類型(例如歸併排序和快速排序)更快,可是我認爲用這種方式去分析算法的確頗有趣。