算法爲王。前端
想學好前端,先練好內功,只有內功深厚者,前端之路纔會走得更遠。git
筆者寫的 JavaScript 數據結構與算法之美 系列用的語言是 JavaScript ,旨在入門數據結構與算法和方便之後複習。github
之因此把冒泡排序、選擇排序、插入排序
放在一塊兒比較,是由於它們的平均時間複雜度都爲 O(n2)。算法
請你們帶着問題:爲何插入排序比冒泡排序更受歡迎 ?
來閱讀下文。數組
複雜度分析是整個算法學習的精髓。數據結構
時間和空間複雜度的詳解,請看 JavaScript 數據結構與算法之美 - 時間和空間複雜度。工具
學習排序算法,咱們除了學習它的算法原理、代碼實現以外,更重要的是要學會如何評價、分析一個排序算法。post
分析一個排序算法,要從 執行效率
、內存消耗
、穩定性
三方面入手。性能
1. 最好狀況、最壞狀況、平均狀況時間複雜度學習
咱們在分析排序算法的時間複雜度時,要分別給出最好狀況、最壞狀況、平均狀況下的時間複雜度。
除此以外,你還要說出最好、最壞時間複雜度對應的要排序的原始數據是什麼樣的。
2. 時間複雜度的係數、常數 、低階
咱們知道,時間複雜度反應的是數據規模 n 很大的時候的一個增加趨勢,因此它表示的時候會忽略係數、常數、低階。
可是實際的軟件開發中,咱們排序的多是 10 個、100 個、1000 個這樣規模很小的數據,因此,在對同一階時間複雜度的排序算法性能對比的時候,咱們就要把係數、常數、低階也考慮進來。
3. 比較次數和交換(或移動)次數
這一節和下一節講的都是基於比較的排序算法。基於比較的排序算法的執行過程,會涉及兩種操做,一種是元素比較大小,另外一種是元素交換或移動。
因此,若是咱們在分析排序算法的執行效率的時候,應該把比較次數和交換(或移動)次數也考慮進去。
也就是看空間複雜度。
還須要知道以下術語:
相等
的元素,通過排序以後,相等元素之間原有的前後順序不變
。相等
的元素,通過排序以後,相等元素之間原有的前後順序改變
。思想
特色
實現
// 冒泡排序(未優化) const bubbleSort = arr => { console.time('改進前冒泡排序耗時'); const length = arr.length; if (length <= 1) return; // i < length - 1 是由於外層只須要 length-1 次就排好了,第 length 次比較是多餘的。 for (let i = 0; i < length - 1; i++) { // j < length - i - 1 是由於內層的 length-i-1 到 length-1 的位置已經排好了,不須要再比較一次。 for (let j = 0; j < length - i - 1; j++) { if (arr[j] > arr[j + 1]) { const temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } console.log('改進前 arr :', arr); console.timeEnd('改進前冒泡排序耗時'); };
優化:當某次冒泡操做已經沒有數據交換時,說明已經達到徹底有序,不用再繼續執行後續的冒泡操做。
// 冒泡排序(已優化) const bubbleSort2 = arr => { console.time('改進後冒泡排序耗時'); const length = arr.length; if (length <= 1) return; // i < length - 1 是由於外層只須要 length-1 次就排好了,第 length 次比較是多餘的。 for (let i = 0; i < length - 1; i++) { let hasChange = false; // 提早退出冒泡循環的標誌位 // j < length - i - 1 是由於內層的 length-i-1 到 length-1 的位置已經排好了,不須要再比較一次。 for (let j = 0; j < length - i - 1; j++) { if (arr[j] > arr[j + 1]) { const temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; hasChange = true; // 表示有數據交換 } } if (!hasChange) break; // 若是 false 說明全部元素已經到位,沒有數據交換,提早退出 } console.log('改進後 arr :', arr); console.timeEnd('改進後冒泡排序耗時'); };
測試
// 測試 const arr = [7, 8, 4, 5, 6, 3, 2, 1]; bubbleSort(arr); // 改進前 arr : [1, 2, 3, 4, 5, 6, 7, 8] // 改進前冒泡排序耗時: 0.43798828125ms const arr2 = [7, 8, 4, 5, 6, 3, 2, 1]; bubbleSort2(arr2); // 改進後 arr : [1, 2, 3, 4, 5, 6, 7, 8] // 改進後冒泡排序耗時: 0.318115234375ms
分析
原地
排序算法。穩定
的排序算法。動畫
插入排序又爲分爲 直接插入排序 和優化後的 拆半插入排序 與 希爾排序,咱們一般說的插入排序是指直接插入排序。
1、直接插入
思想
通常人打撲克牌,整理牌的時候,都是按牌的大小(從小到大或者從大到小)整理牌的,那每摸一張新牌,就掃描本身的牌,把新牌插入到相應的位置。
插入排序的工做原理:經過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。
步驟
實現
// 插入排序 const insertionSort = array => { const len = array.length; if (len <= 1) return let preIndex, current; for (let i = 1; i < len; i++) { preIndex = i - 1; //待比較元素的下標 current = array[i]; //當前元素 while (preIndex >= 0 && array[preIndex] > current) { //前置條件之一: 待比較元素比當前元素大 array[preIndex + 1] = array[preIndex]; //將待比較元素後移一位 preIndex--; //遊標前移一位 } if (preIndex + 1 != i) { //避免同一個元素賦值給自身 array[preIndex + 1] = current; //將當前元素插入預留空位 console.log('array :', array); } } return array; };
測試
// 測試 const array = [5, 4, 3, 2, 1]; console.log("原始 array :", array); insertionSort(array); // 原始 array: [5, 4, 3, 2, 1] // array: [4, 5, 3, 2, 1] // array: [3, 4, 5, 2, 1] // array: [2, 3, 4, 5, 1] // array: [1, 2, 3, 4, 5]
分析
原地
排序算法。穩定
的排序算法。動畫
2、拆半插入
插入排序也有一種優化算法,叫作拆半插入
。
思想
折半插入排序是直接插入排序的升級版,鑑於插入排序第一部分爲已排好序的數組, 咱們沒必要按順序依次尋找插入點, 只需比較它們的中間值與待插入元素的大小便可。
步驟
注:x>>1 是位運算中的右移運算,表示右移一位,等同於 x 除以 2 再取整,即 x>>1 == Math.floor(x/2) 。
// 折半插入排序 const binaryInsertionSort = array => { const len = array.length; if (len <= 1) return; let current, i, j, low, high, m; for (i = 1; i < len; i++) { low = 0; high = i - 1; current = array[i]; while (low <= high) { //步驟 1 & 2 : 折半查找 m = (low + high) >> 1; // 注: x>>1 是位運算中的右移運算, 表示右移一位, 等同於 x 除以 2 再取整, 即 x>>1 == Math.floor(x/2) . if (array[i] >= array[m]) { //值相同時, 切換到高半區,保證穩定性 low = m + 1; //插入點在高半區 } else { high = m - 1; //插入點在低半區 } } for (j = i; j > low; j--) { //步驟 3: 插入位置以後的元素所有後移一位 array[j] = array[j - 1]; console.log('array2 :', JSON.parse(JSON.stringify(array))); } array[low] = current; //步驟 4: 插入該元素 } console.log('array2 :', JSON.parse(JSON.stringify(array))); return array; };
測試
const array2 = [5, 4, 3, 2, 1]; console.log('原始 array2:', array2); binaryInsertionSort(array2); // 原始 array2: [5, 4, 3, 2, 1] // array2 : [5, 5, 3, 2, 1] // array2 : [4, 5, 5, 2, 1] // array2 : [4, 4, 5, 2, 1] // array2 : [3, 4, 5, 5, 1] // array2 : [3, 4, 4, 5, 1] // array2 : [3, 3, 4, 5, 1] // array2 : [2, 3, 4, 5, 5] // array2 : [2, 3, 4, 4, 5] // array2 : [2, 3, 3, 4, 5] // array2 : [2, 2, 3, 4, 5] // array2 : [1, 2, 3, 4, 5]
注意
:和直接插入排序相似,折半插入排序每次交換的是相鄰的且值爲不一樣的元素,它並不會改變值相同的元素之間的順序,所以它是穩定的。
3、希爾排序
希爾排序是一個平均時間複雜度爲 O(nlogn) 的算法,會在下一個章節和 歸併排序、快速排序、堆排序 一塊兒講,本文就不展開了。
思路
選擇排序算法的實現思路有點相似插入排序,也分已排序區間和未排序區間。可是選擇排序每次會從未排序區間中找到最小的元素,將其放到已排序區間的末尾。
步驟
實現
const selectionSort = array => { const len = array.length; let minIndex, temp; for (let i = 0; i < len - 1; i++) { minIndex = i; for (let j = i + 1; j < len; j++) { if (array[j] < array[minIndex]) { // 尋找最小的數 minIndex = j; // 將最小數的索引保存 } } temp = array[i]; array[i] = array[minIndex]; array[minIndex] = temp; console.log('array: ', array); } return array; };
測試
// 測試 const array = [5, 4, 3, 2, 1]; console.log('原始array:', array); selectionSort(array); // 原始 array: [5, 4, 3, 2, 1] // array: [1, 4, 3, 2, 5] // array: [1, 2, 3, 4, 5] // array: [1, 2, 3, 4, 5] // array: [1, 2, 3, 4, 5]
分析
原地
排序算法。不穩定
的排序算法。動畫
爲何插入排序比冒泡排序更受歡迎 ?
冒泡排序和插入排序的時間複雜度都是 O(n2),都是原地排序算法,爲何插入排序要比冒泡排序更受歡迎呢 ?
這裏關乎到 逆序度、滿有序度、有序度。
有序元素對:a[i] <= a[j], 若是 i < j。
滿有序度:把徹底有序的數組的有序度叫做 滿有序度。
逆序元素對:a[i] > a[j], 若是 i < j。
同理,對於一個倒序排列的數組,好比 6,5,4,3,2,1,有序度是 0;
對於一個徹底有序的數組,好比 1,2,3,4,5,6,有序度就是 **n*(n-1)/2** ,也就是滿有序度爲 15。
緣由
複雜性對比
名稱 | 平均 | 最好 | 最壞 | 空間 | 穩定性 | 排序方式 |
---|---|---|---|---|---|---|
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | Yes | In-place |
插入排序 | O(n2) | O(n) | O(n2) | O(1) | Yes | In-place |
選擇排序 | O(n2) | O(n2) | O(n2) | O(1) | No | In-place |
算法可視化工具
這裏推薦一個算法可視化工具。
算法可視化工具 algorithm-visualizer 是一個交互式的在線平臺,能夠從代碼中可視化算法,還能夠看到代碼執行的過程。
效果以下圖。
喜歡就點個小星星吧。
文中全部的代碼及測試事例都已經放到個人 GitHub 上了。
參考文章: