前端工程師因爲業務特色比較少接觸算法的東西,因此本系列也不會講太過深刻的東西,更多的是做爲知識擴展和思惟邏輯的培養。
排序就是將一組對象按照某種邏輯順序從新排列的過程,本篇將介紹幾種金典的排序算法。javascript
在計算時代早期,你們廣泛認爲30%的計算週期都用在了排序上。若是今天這個比例下降了,可能的緣由之一是現在的排序算法更加高效,而並不是排序的重要性下降了。
約定都是從小到大排序,當前項爲i。swap是交換數組內位置的函數,實現以下:前端
function swap(_arr, index1, index2) { const arr = _arr; arr[index1] += arr[index2]; arr[index2] = arr[index1] - arr[index2]; arr[index1] -= arr[index2]; }
學校裏第一個學的排序方式老是冒泡排序,雖然它效率低,但最容易理解。冒泡排序比較任何兩個相鄰的項,若是第一個比第二個大,則交換它們。元素項向上移動至正確的順序,就好像氣泡升至表面同樣,冒泡排序所以得名。java
基本思路:算法
代碼實現:數組
function bubbleSort(_arr) { const arr = [].slice.call(_arr); const len = arr.length; for (let i = 0; i < len; i += 1) { for (let f = 0; f < len - 1; f += 1) { if (arr[f] > arr[f + 1]) { swap(arr, f, f + 1); } } } return arr; }
示例過程:緩存
// 初始 5 4 9 5 3 // 第一趟 4 5 9 5 3 // 5>4,交換 ^ ^ 4 5 9 5 3 // 5<9,不變 ^ ^ 4 5 5 9 3 // 9>5,交換 ^ ^ 4 5 5 3 9 // 9>3,交換 ^ ^ // 第二趟 4 5 5 3 9 // 4<5,不變 ^ ^ 4 5 5 3 9 // 5=5,不變 ^ ^ 4 5 3 5 9 // 5>3,交換 ^ ^ 4 5 3 5 9 // 5<9,不變 ^ ^ // 第三趟 4 5 3 5 9 // 4<5,不變 ^ ^ 4 3 5 5 9 // 5>3,交換 ^ ^ 4 3 5 5 9 // 5=5,不變 ^ ^ 4 3 5 5 9 // 5<9,不變 ^ ^ // 第四趟 3 4 5 5 9 // 4>3,交換 ^ ^ 3 4 5 5 9 // 4<5,不變 ^ ^ 3 4 5 5 9 // 5=5,不變 ^ ^ 3 4 5 5 9 // 5<9,不變 ^ ^ // 第五趟 3 4 5 5 9 // 3<4,不變 ^ ^ 3 4 5 5 9 // 4<5,不變 ^ ^ 3 4 5 5 9 // 5=5,不變 ^ ^ 3 4 5 5 9 // 5<9,不變 ^ ^ // 結果 3 4 5 5 9
經過上面的排序過程,能夠發現其實每一趟就能夠肯定最後一位的位置了,因此能夠不用再比較最後的位置。代碼改造也很小,只要在內循環減去已經肯定的位置數便可。前端工程師
function modifiedBubbleSort(_arr) { const arr = [].slice.call(_arr); const len = arr.length; for (let i = 0; i < len; i += 1) { for (let f = 0; f < len - i - 1; f += 1) { if (arr[f] > arr[f + 1]) { swap(arr, f, f + 1); } } } return arr; }
示例過程:dom
// 初始 5 4 9 5 3 // 第一趟 4 5 9 5 3 // 5>4,交換 ^ ^ 4 5 9 5 3 // 5<9,不變 ^ ^ 4 5 5 9 3 // 9>5,交換 ^ ^ 4 5 5 3 9 // 9>3,交換 ^ ^ // 第二趟 4 5 5 3 9 // 4<5,不變 ^ ^ 4 5 5 3 9 // 5=5,不變 ^ ^ 4 5 3 5 9 // 5>3,交換 ^ ^ // 第三趟 4 5 3 5 9 // 4<5,不變 ^ ^ 4 3 5 5 9 // 5>3,交換 ^ ^ // 第四趟 3 4 5 5 9 // 4>3,交換 ^ ^ // 結果 3 4 5 5 9
選擇排序算法是一種原址比較排序算法。這也是比較簡單的過程,只要不斷遍歷找到最小的數依次放入位置便可。
基本思路:函數
代碼實現:性能
function selectionSort(_arr) { const arr = [].slice.call(_arr); const len = arr.length; for (let i = 0; i < len - 1; i += 1) { let indexMin = i; for (let f = i + 1; f < len; f += 1) { if (arr[indexMin] > arr[f]) { indexMin = f; } } if (indexMin !== i) { swap(arr, indexMin, i); } } return arr; }
示例過程:
// 初始 5 4 9 5 3 // 第一趟,指針指向0號位 5 4 9 5 3 // 4<5,指針指向1號位 ^ 5 4 9 5 3 // 9>4,指針不變 ^ 5 4 9 5 3 // 5>4,指針不變 ^ 5 4 9 5 3 // 3<4,指針指向4號位 ^ 3 4 9 5 5 // 遍歷結束,交換0號位和4號位 // 第二趟,指針指向1號位 3 4 9 5 5 // 9>4,指針不變 ^ 3 4 9 5 5 // 5>4,指針不變 ^ 3 4 9 5 5 // 5>4,指針不變 ^ 3 4 9 5 5 // 遍歷結束,1號位不變 // 第三趟,指針指向2號位 3 4 9 5 5 // 5<9,指針指向3號位 ^ 3 4 9 5 5 // 5=5,指針不變 ^ 3 4 5 9 5 // 遍歷結束,交換2號位和3號位 // 第四趟,指針指向3號位 3 4 5 9 5 // 5<9,指針指向4號位 ^ 3 4 5 5 9 // 遍歷結束,交換3號位和4號位 // 結果 3 4 5 5 9
插入排序就是要把後面的數往前面插入。假定第一項已經排序了,接着從第二項開始,依次判斷當前項應該插入到前面的哪一個位置。
基本思路:
代碼實現:
function insertionSort(_arr) { const arr = [].slice.call(_arr); const len = arr.length; for (let i = 1; i < len; i += 1) { let f = i; const temp = arr[i]; while (f > 0 && arr[f - 1] > temp) { arr[f] = arr[f - 1]; f -= 1; } arr[f] = temp; } return arr; }
示例過程:
// 初始 5 4 9 5 3 // 第一趟,當前項是1號位,數字4 _ 5 9 5 3 // 4<5,5向後移動 ^ ^ 4 5 9 5 3 // 遍歷結束,寫入4 ^ // 第二趟,當前項是2號位,數字9 4 5 9 5 3 // 9>5,不變 ^ 4 5 9 5 3 // 9>4,不變,遍歷結束 ^ // 第三趟,當前項是3號位,數字5 4 5 _ 9 3 // 5<9,9向後移動 ^ ^ 4 5 _ 9 3 // 5=5,不變 ^ 4 5 _ 9 3 // 5>4,不變 ^ 4 5 5 9 3 // 遍歷結束,寫入5 ^ // 第四趟,當前項是4號位,數字3 4 5 5 _ 9 // 3<9,9向後移動 ^ ^ 4 5 _ 5 9 // 3<5,5向後移動 ^ ^ 4 _ 5 5 9 // 3<5,5向後移動 ^ ^ _ 4 5 5 9 // 3<4,4向後移動 ^ ^ 3 4 5 5 9 // 遍歷結束,寫入3 ^ // 結果 3 4 5 5 9
歸併排序是一種分治算法。其思想是將原始數組切分紅較小的數組,直到每一個小數組只有一個位置,接着將小數組歸併成較大的數組,直到最後只有一個排序完畢的大數組。
基本思路:
代碼實現:
function mergeSort(_arr) { const arr = [].slice.call(_arr); function merge(left, right) { const result = []; let iL = 0; let iR = 0; const lenL = left.length; const lenR = right.length; while (iL < lenL && iR < lenR) { if (left[iL] < right[iR]) { result.push(left[iL]); iL += 1; } else { result.push(right[iR]); iR += 1; } } while (iL < lenL) { result.push(left[iL]); iL += 1; } while (iR < lenR) { result.push(right[iR]); iR += 1; } return result; } return (function cut(_array) { const len = _array.length; if (len === 1) { return _array; } const mid = Math.floor(len / 2); const left = _array.slice(0, mid); const right = _array.slice(mid, len); return merge(cut(left), cut(right)); }(arr)); }
示例過程:
// 初始 5 4 9 5 3 // 切分 [5 4] [9 5 3] // 中間數是9 ^ ([5] [4]) [9 5 3] // 進入左側數組,中間數是4 ^ ([5] [4]) ([9] [5 3]) // 左側切分完,進入右側數組,中間數是5 ^ ([5] [4]) ([9] ([5] [3])) // 左側切分完,進入右側數組,中間數是3 ^ // 合併[5]和[3] ([5] [4]) ([9] [3 $]) // 3<5,入3 ^ ([5] [4]) ([9] [3 5]) // 入5,完畢 ^ // 合併[9]和[3 5] ([5] [4]) [3 $ $] // 3<9,入3 ^ ([5] [4]) [3 5 $] // 5<9,入5 ^ ([5] [4]) [3 5 9] // 入9,完畢 ^ // 合併[5]和[4] [4 $] [3 5 9] // 4<5,入4 ^ [4 5] [3 5 9] // 入5,完畢 ^ // 合併[4 5]和[3 5 9] [3 $ $ $ $] // 4>3,入3 ^ [3 4 $ $ $] // 4<5,入4 ^ [3 4 5 $ $] // 5=5,入5 ^ [3 4 5 5 $] // 入5 ^ [3 4 5 5 9] // 入9,完畢 ^ // 結果 3 4 5 5 9
快速排序的思想跟歸併很像,都是分治方法,但它沒有像歸併排序那樣將它們分割開,而是使用指針遊標來標記,每次會肯定一個主元的位置。稍微會比前面的複雜一些。
基本思路:
代碼實現:
function quickSort(_arr) { const arr = [].slice.call(_arr); function partition(low, high) { const pivotkey = arr[low]; let i = low; let j = high; while (i < j) { while (i < j && arr[j] >= pivotkey) { j -= 1; } arr[i] = arr[j]; while (i < j && arr[i] <= pivotkey) { i += 1; } arr[j] = arr[i]; } arr[i] = pivotkey; return i; } (function QSort(low, high) { if (low < high) { const pivotloc = partition(low, high); QSort(low, pivotloc - 1); QSort(pivotloc + 1, high); } }(0, arr.length - 1)); return arr; }
示例過程:
// 初始 5 4 9 5 3 // 第一趟,主元爲5 5 4 9 5 3 // high開始移動,3<5,high中止 ^L ^H 3 4 9 5 3 // 將high指向數3寫入到low位置 ^L ^H 3 4 9 5 3 // low開始移動,3<5,繼續前進 ^L ^H 3 4 9 5 3 // 4<5,繼續前進 ^L ^H 3 4 9 5 3 // 9>5,low中止 ^L ^H 3 4 9 5 9 // 將low指向數9寫入到high位置 ^L ^H 3 4 9 5 9 // high開始移動,9>5,繼續後退 ^L ^H 3 4 9 5 9 // high開始移動,5=5,繼續後退 ^L^H 3 4 5 5 9 // 兩指針重合,結束,肯定主元5的位置,寫入 * // 第二趟,主元爲3 3 4 5 5 9 // high開始移動,4>3,繼續後退 ^L^H* 3 4 5 5 9 // 兩指針重合,結束,肯定主元3的位置,寫入 * * // 第三趟,主元爲4 3 4 5 5 9 // 兩指針重合,結束,肯定主元4的位置,寫入 * * * // 第四趟,主元爲5 3 4 5 5 9 // high開始移動,9>5,繼續後退 * * * ^L^H 3 4 5 5 9 // 兩指針重合,結束,肯定主元5的位置,寫入 * * * * // 第五趟,主元爲9 3 4 5 5 9 // 兩指針重合,結束,肯定主元9的位置,寫入 * * * * * // 結果 3 4 5 5 9
上述的這麼多種排序算法哪一個比較快?這是咱們比較好奇的問題,咱們隨機生成10000個數據來測試一下吧。
兩個輔助函數:getRandomArray用來生成隨機數的數組,costClock用來統計耗時。
function getRandomArray(len = 10000, min = 0, max = 100) { const array = []; const w = max - min; for (let i = 0; i < len; i += 1) { array.push(parseInt((Math.random() * w) + min, 10)); } return array; } function costClock(fn) { const now = new Date().getTime(); const data = fn(); const pass = new Date().getTime() - now; return { data, cost: pass, }; }
測試用例以下:
const array = getRandomArray(10000); const result1 = costClock(() => bubbleSort(array)); const result2 = costClock(() => modifiedBubbleSort(array)); const result3 = costClock(() => selectionSort(array)); const result4 = costClock(() => insertionSort(array)); const result5 = costClock(() => mergeSort(array)); const result6 = costClock(() => quickSort(array)); console.log(result1); console.log(result2); console.log(result3); console.log(result4); console.log(result5); console.log(result6);
結果以下圖,可見快速排序不愧是快速排序,不須要交互數據以及分治方法是其高效的主要緣由。