面試官:小夥子排序算法瞭解嗎?javascript
回答:我能寫出來四種冒泡排序,兩種選擇排序,兩種插入排序,兩種哈希排序,兩種歸併排序,兩種堆排序,四種快速排序。html
用我本身的方式。前端
文中全部代碼位於位於此代碼倉庫中,推薦下載代碼進行練習、推敲。java
(已過時)號外:博主爲 18 屆應屆生,目前狀態是前端開發補招進行時。若有內推機會,歡迎一波流帶走 :》git
更多信息,歡迎check: rayjune.me/aboutes6
另,若是以爲這些用心推敲的代碼對你有幫助的話,歡迎 star 一下代碼倉庫,衆籌博主找到一份體面的工做,在這裏給你們遞茶了:)github
P.S. 原文顯示效果更好喔:) check:rayjune.me/優雅的 JavaScr…面試
做者:RayJune(轉載請署名,請尊重博主含辛茹苦、遍查資料、一行一行含淚碼出來的成果。參考&感謝 部分裏代碼參考地址都已列出)算法
另,本文中常使用 swap
函數,在這裏提早列出來,如下就省略了。shell
function swap(arr, indexA, indexB) {
[arr[indexA], arr[indexB]] = [arr[indexB], arr[indexA]];
}
複製代碼
經過依次比較、交換相鄰的元素大小(按照由小到大的順序,若是符合這個順序就不用交換)。
1 次這樣的循環能夠獲得一個最大值,n - 1
次這樣的循環能夠排序完畢。
O(n²)
O(n²)
O(n)
(可是這種狀況下不如插入排序塊,請繼續看下文)postion
來優化function bubbleSort(arr) {
for (let i = arr.length - 1; i > 0; i--) {
for (let j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
return arr;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(bubbleSort(arr));
複製代碼
設置一標誌性變量 pos
,用於記錄每趟排序中最後一次進行交換的位置。 因爲 pos
位置以後的記錄均已交換到位,故在進行下一趟排序時只要掃描到 pos
位置便可。
function bubbleSort2(arr) {
let i = arr.length - 1;
while (i > 0) {
let pos = 0;
for (let j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
pos = j;
swap(arr, j, j + 1);
}
}
i = pos;
}
return arr;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(bubbleSort2(arr));
複製代碼
傳統冒泡排序中每一趟排序操做只能找到一個最大值或最小值, 咱們能夠 在每趟排序中進行正向和反向兩遍冒泡 , 一次能夠獲得兩個最終值(最大和最小) , 從而使外排序趟數幾乎減小了一半。
function bubbleSort3(arr) {
let start = 0;
let end = arr.length - 1;
while (start < end) {
for (let i = start; i < end; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
end -= 1;
for (let i = end; i > start; i--) {
if (arr[i - 1] > arr[i]) {
swap(arr, i - 1, i);
}
}
start += 1;
}
return arr;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(bubbleSort3(arr));
複製代碼
前兩種優化方式(緩存 pos
、雙向遍歷)的結合:
function bubbleSort4(arr) {
let start = 0;
let end = arr.length - 1;
while (start < end) {
let endPos = 0;
let startPos = 0;
for (let i = start; i < end; i++) {
if (arr[i] > arr[i + 1]) {
endPos = i;
swap(arr, i, i + 1);
}
}
end = endPos;
for (let i = end; i > start; i--) {
if (arr[i - 1] > arr[i]) {
startPos = i;
swap(arr, i - 1, i);
}
}
start = startPos;
}
return arr;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(bubbleSort4(arr));
複製代碼
來自於螞蟻金服的一道面試題:
對於冒泡排序來講,能不能傳入第二個參數(參數爲函數),來控制升序和降序?(聯想一下
array.sort()
)
function bubbleSort(arr, compareFunc) {
for (let i = arr.length - 1; i > 0; i--) {
for (let j = 0; j < i; j++) {
if (compareFunc(arr[j], arr[j + 1]) > 0) {
swap(arr, j, j + 1);
}
}
}
return arr;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(bubbleSort(arr, (a, b) => a - b));
console.log(bubbleSort(arr, (a, b) => b - a));
複製代碼
每一次內循環遍歷尋找最小的數,記錄下 minIndex
,並在此次內循環結束後交換 minIndex
和 i
的位置。
重複這樣的循環 n - 1
次即獲得結果。
Θ(n²)
不管什麼輸入,均爲 Θ(n²)
Θ(n) 交換
: 注意,這裏只有 n
次的交換,選擇排序的惟一優勢*關於 Θ(n) swaps:
Selection sort has the property of minimizing the number of swaps. In applications where the cost of swapping items is high, selection sort very well may be the algorithm of choice.
可見即便是咱們以爲最慢的選擇排序,也有它的用武之地。
O(n²)
,但不穩定,惟一的優勢是減小了 swap
次數。function selectionSort(arr) {
for (let i = 0, len = arr.length; i < len - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (i !== minIndex) {
swap(arr, i, minIndex);
}
}
return arr;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(selectionSort(arr));
複製代碼
若是你想在每次內循環中找到最大值並把其交換到數組的末尾(相比較 minIndex
有點麻煩),如下是實現的代碼:
function selectionSort2(arr) {
for (let i = arr.length - 1; i > 0; i--) {
let maxIndex = i;
for (let j = i - 1; j >= 0; j--) {
if (arr[j] > arr[maxIndex]) {
maxIndex = j;
}
}
if (i !== maxIndex) {
swap(arr, i, maxIndex);
}
}
return arr;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(selectionSort2(arr));
複製代碼
默認 a[0]
爲已排序數組中的元素,從 arr[1]
開始逐漸往已排序數組中插入元素,從後往前一個個比較,若是待插入元素小於已排序元素,則已排序元素日後移動一位,直到待插入元素找到合適的位置並插入已排序數組。
通過 n - 1
次這樣的循環插入後排序完畢。
O(n)
O(n²)
因爲它的優勢(自適應,低開銷,穩定,幾乎排序時的
O(n)
時間),插入排序一般用做遞歸基本狀況(當問題規模較小時)針對較高開銷分而治之排序算法, 如希爾排序或快速排序。
function insertionSort(arr) {
for (let i = 1, len = arr.length; i < len; i++) {
const temp = arr[i];
let preIndex = i - 1;
while (arr[preIndex] > temp) {
arr[preIndex + 1] = arr[preIndex];
preIndex -= 1;
}
arr[preIndex + 1] = temp;
}
return arr;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(insertionSort(arr));
複製代碼
由於對於插入排序的優化方法是二分查找優化,這裏補充一下二分查找的算法的實現。
核心概念是:折半。
function binarySearch(arr, value) {
let min = 0;
let max = arr.length - 1;
while (min <= max) {
const mid = Math.floor((min + max) / 2);
if (arr[mid] === value) {
return mid;
} else if (arr[mid] > value) {
max = mid - 1;
} else {
min = mid + 1;
}
}
return 'Not Found';
}
// test
const arr = [1, 2, 3];
console.log(binarySearch(arr, 2)); // 1
console.log(binarySearch(arr, 4)); // Not Found
複製代碼
首先把二分查找算法作一點小修改,以適應咱們的插入排序:
function binarySearch(arr, maxIndex, value) {
let min = 0;
let max = maxIndex;
while (min <= max) {
const mid = Math.floor((min + max) / 2);
if (arr[mid] <= value) {
min = mid + 1;
} else {
max = mid - 1;
}
}
return min;
}
複製代碼
而後在查找插入位置時使用二分查找的方式來優化性能:
function insertionSort2(arr) {
for (let i = 1, len = arr.length; i < len; i++) {
const temp = arr[i];
const insertIndex = binarySearch(arr, i - 1, arr[i]);
for (let preIndex = i - 1; preIndex >= insertIndex; preIndex--) {
arr[preIndex + 1] = arr[preIndex];
}
arr[insertIndex] = temp;
}
return arr;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(insertionSort2(arr));
複製代碼
希爾排序是插入排序的改進版,它克服了插入排序只能移動一個相鄰位置的缺陷(希爾排序能夠一次移動 gap
個距離),利用了插入排序在排序幾乎已經排序好的數組的很是快的優勢。
使用能夠動態定義的 gap
來漸進式排序,先排序距離較遠的元素,再逐漸遞進,而實際上排序中元素最終位置距離初始位置遠的機率是很大的,因此希爾排序大大提高了性能(尤爲是 reverse 的時候很是快,想象一下這時候冒泡排序和插入排序的速度)。
並且希爾排序不只效率較高(比冒泡和插入高),它的代碼相對要簡短,低開銷(繼承插入排序的優勢),追求這些特色(效率要求過得去就好,代碼簡短,開銷低,且數據量較小)的時候希爾排序是好的 O(n·log(n))
算法的替代品。
總而言之:希爾排序的性能優化來自增量隊列的輸入和 gap
的設定。
O(n·log(n))
的時間複雜度(而且它對於反轉數組的速度很是快)O(n^3/2)
time as shown (想要了解更多細節,請查閱 wikipedia Shellsort)關於不穩定:
咱們知道, 單次直接插入排序是穩定的,它不會改變相同元素之間的相對順序,但在屢次不一樣的插入排序過程當中, 相同的元素可能在各自的插入排序中移動,可能致使相同元素相對順序發生變化。所以, 希爾排序並不穩定。
關於 worse-case time 有一點複雜:
The worse-case time complexity of shell sort depends on the increment sequence. For the increments 1 4 13 40 121…, which is what is used here, the time complexity is O(n3/2). For other increments, time complexity is known to be O(n4/3) and even O(n·log2(n)).
希爾排序是基於插入排序的如下兩點性質而提出改進方法的:
O(n)
的效率;其中 gap
(增量)的選擇是希爾排序的重要部分。只要最終 gap
爲 1 任何 gap
序列均可以工做。算法最開始以必定的 gap
進行排序。而後會繼續以必定 gap
進行排序,直到 gap = 1
時,算法變爲插入排序。
Donald Shell 最初建議 gap
選擇爲 n / 2
而且對 gap
取半直到 gap
達到 1 。雖然這樣取能夠比 O(n²) 類的算法(插入排序、冒泡排序)更好,但這樣仍然有減小平均時間和最差時間的餘地。 (關於優化 gap
的細節涉及到複雜的數學知識,咱們這裏不作深究,詳細能夠參考 wikipedia 上的頁面)
Donald Shell 的最初建議(gap = n / 2
)版代碼(方便理解):
function shellSort(arr) {
const len = arr.length;
let gap = Math.floor(len / 2);
while (gap > 0) {
// 注意下面這段 for 循環和插入排序極爲類似
for (let i = gap; i < len; i++) {
const temp = arr[i];
let preIndex = i - gap;
while (arr[preIndex] > temp) {
arr[preIndex + gap] = arr[preIndex];
preIndex -= gap;
}
arr[preIndex + gap] = temp;
}
gap = Math.floor(gap / 2);
}
return arr;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(shellSort(arr));
複製代碼
常見的、易生成的、優化 gap
的序列方法(來自 Algorithms (4th Edition) ,有些更快的方法但序列不容易生成,由於用到了比較深奧的數學公式):
function shellSort(arr) {
const len = arr.length;
let gap = 1;
while (gap < len / 3) {
gap = gap * 3 + 1;
}
while (gap > 0) {
for (let i = gap; i < len; i++) {
const temp = arr[i];
let preIndex = i - gap;
while (arr[preIndex] > temp) {
arr[preIndex + gap] = arr[preIndex];
preIndex -= gap;
}
arr[preIndex + gap] = temp;
}
gap = Math.floor(gap / 2);
}
return arr;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(shellSort(arr));
複製代碼
歸併排序使用分而治之的思想,以折半的方式來遞歸/迭代排序元素,利用空間來換時間,作到了時間複雜度 O(n·log(n))
的同時保持了穩定。
這讓它在一些更考慮排序效率和穩定性,次考慮存儲空間的場合很是適用(如數據庫內排序,和堆排序相比,歸併排序的穩定是優勢)。而且歸併排序很是適合於鏈表排序。
O(n·log(n))
時間複雜度的排序算法中,歸併排序是惟一穩定的)O(n·log(n))
以迭代的方式來實現(但要注意防止函數調用過深致使 JavaScript 的運行棧溢出):
function mergeSort(arr) {
const len = arr.length;
if (len < 2) { return arr; }
const mid = Math.floor(len / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right) {
const result = [];
while (left.length > 0 && right.length > 0) {
result.push(left[0] <= right[0] ? left.shift() : right.shift());
}
return result.concat(left, right);
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(mergeSort(arr));
複製代碼
用 array.splice
取代 array.slice
,減小一半的空間消耗。
function mergeSort2(arr) {
const len = arr.length;
if (len < 2) { return arr; }
const mid = Math.floor(len / 2);
const left = arr.splice(0, mid);
const right = arr;
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right) {
const result = [];
while (left.length > 0 && right.length > 0) {
result.push(left[0] <= right[0] ? left.shift() : right.shift());
}
return result.concat(left, right);
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(mergeSort2(arr));
複製代碼
堆排序能夠認爲是選擇排序的改進版,像選擇排序同樣將輸入劃分爲已排序和待排序。
不同的是堆排序利用堆這種近似徹底二叉樹的良好的數據結構來實現排序,本質上使用了二分的思想。
arr[0]
到數組末尾(已排序區域)利用堆這種良好的數據結構,它在擁有良好的可預測性的同時(無論輸入什麼都是 O(n·log(n))
時間複雜度),但它的缺點也有:即不穩定,並且 O(n·log(n))
的平均效率決定了它的效率不如快速排序。適用於數據庫內引擎排序(須要這樣的可預測性性能)。
O(n·log(n)
time)function heapSort(arr) {
let size = arr.length;
// 初始化堆,i 從最後一個父節點開始調整,直到節點均調整完畢
for (let i = Math.floor(size / 2) - 1; i >= 0; i--) {
heapify(arr, i, size);
}
// 堆排序:先將第一個元素和已拍好元素前一位做交換,再從新調整,直到排序完畢
for (let i = size - 1; i > 0; i--) {
swap(arr, 0, i);
size -= 1;
heapify(arr, 0, size);
}
return arr;
}
function heapify(arr, index, size) {
let largest = index;
let left = 2 * index + 1;
let right = 2 * index + 2;
if (left < size && arr[left] > arr[largest]) {
largest = left;
}
if (right < size && arr[right] > arr[largest]) {
largest = right;
}
if (largest !== index) {
swap(arr, index, largest);
heapify(arr, largest, size);
}
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(heapSort(arr));
複製代碼
wikipedia 上給出的方法於初版的區別在於維護堆性質時採用的方式不一樣,本質是同樣的:
function heapSort(arr) {
const size = arr.length;
// 初始化 heap,i 從最後一個父節點開始調整,直到節點均調整完畢
for (let i = Math.floor(size / 2) - 1; i >= 0; i--) {
heapify(i, size);
}
// 堆排序:先將第一個元素和已拍好元素前一位做交換,再從新調整,直到排序完畢
for (let i = size - 1; i > 0; i--) {
swap(arr, 0, i);
heapify(0, i);
}
return arr;
}
function heapify(start, end) {
// 創建父節點下標和子節點下標
const dad = start;
let son = dad * 2 + 1;
if (son >= end) { return 0; }
if (son + 1 < end && arr[son] < arr[son + 1]){
son += 1;
}
if (arr[dad] <= arr[son]) {
swap(arr, dad, son);
heapify(son, end);
}
return 0;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(heapSort(arr));
複製代碼
When implemented well, it can be about two or three times faster than its main competitors, merge sort and heap sort
function quickSort(arr) {
const pivot = arr[0];
const left = [];
const right = [];
if (arr.length < 2) { return arr; }
for (let i = 1, len = arr.length; i < len; i++) {
arr[i] < pivot ? left.push(arr[i]) : right.push(arr[i]);
}
return quickSort(left).concat([pivot], quickSort(right));
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(quickSort(arr));
複製代碼
函數式編程:結構清晰,一目瞭然。
function quickSort2(arr) {
const pivot = arr.shift();
const left = [];
const right = [];
if (arr.length < 2) { return arr; }
arr.forEach((element) => {
element < pivot ? left.push(element) : right.push(element);
});
return quickSort2(left).concat([pivot], quickSort2(right));
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(quickSort2(arr));
複製代碼
等等,有沒有以爲第1、二版中的代碼雖然看起來簡潔,可是卻對空間消耗很大呢?
由此有了 in-place 版本:
function quickSort3(arr, left = 0, right = arr.length - 1) {
if (left < right) {
const pivot = partition(arr, left, right);
quickSort3(arr, left, pivot - 1);
quickSort3(arr, pivot + 1, right);
}
return arr;
}
function partition (arr, left ,right) {
let pivot = left; // 以第一個元素爲 pivot
for (let i = left + 1; i <= right; i++) {
if (arr[i] < arr[left]) {
swap(arr, i, pivot);
pivot += 1;
}
}
swap(arr, left, pivot); //將 pivot 值移至中間
return pivot;
}
// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(quickSort3(arr));
複製代碼
這一版的亮點是 pivot 的選取,再也不是簡單的取 arr[0]
,而是:
const pivot = left + Math.ceil((right - left) * 0.5)
複製代碼
很是感謝評論區的大神 @Chris_dong 的解釋:
const pivot = left + Math.ceil((right - left) * 0.5)
=> (去掉MAth.ceil是否是很好理解)left + (right - left) * 0.5
=>(right + left) * 0.5
。
看到真相的我眼淚掉下來,原來是取中間值。。。
由此有了如下版本:
function quickSort4(arr, left = 0, right = arr.length - 1) {
if (left < right) {
// const pivot = left + Math.ceil((right - left) * 0.5);
const pivot = Math.floor((right + left) / 2);
const newPivot = partition(arr, pivot, left, right);
quickSort4(arr, left, newPivot - 1);
quickSort4(arr, newPivot + 1, right);
}
return arr;
}
function partition(arr, pivot, left, right) {
const pivotValue = arr[pivot];
let newPivot = left;
swap(arr, pivot, right);
for (let i = left; i < right; i++) {
if (arr[i] < pivotValue) {
swap(arr, i, newPivot);
newPivot += 1;
}
}
swap(arr, right, newPivot);
return newPivot;
}
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(quickSort4(arr));
複製代碼
提出幾個問題,能夠當作自我檢測:
插入排序不解釋
性能大小:希爾排序 > 插入排序 > 冒泡排序 > 選擇排序
O(n²)
的狀況)(如數據庫中)?堆排序
歸併排序
性能大小:快速排序 > 堆排序 > 歸併排序
由於雖然堆排序作到了 O(n·log(n)
,而快速排序的最差狀況是 O(n²)
,可是快速排序的絕大部分時間的效率比 O(n·log(n)
還要快,因此快速排序真的無愧於它的名字。(十分快速)
選擇排序只須要 O(n)
次交換,這一點它完爆冒泡排序。
答疑:
都是博主含辛茹苦、遍查資料、一行一行含淚認真碼出來的。參考&感謝 部分裏列出了全部來源地址:)
實際上這篇文章繼承於優雅的 JavaScript 排序算法 。這一版是上通常的姐妹版(解釋精簡,使用 ES6 使代碼更精簡),若想參考英文引用、ES5 代碼、過程詳細解釋能夠參考初版。
用 ES6
是爲了更強大的表現力,從而讓咱們更加關注於算法的內在,不被一些邊邊角角所束縛。
博主一貫認爲是有【代碼品味】這種東西存在的,能夠從以前的這篇文章從 shuffle 看代碼品味一窺端倪。
再次表達一下本身的觀點:
可是追求的最終目的是一致的:好讀又簡潔,穩定易維護。
爲了這個目標我作了這些努力:
preIndex
, temp
, size
;len / 2
的取整時爲了可讀性選擇了 Math.floor(len / 2)
,沒有選擇 len >> 1
和 parseInt(len / 2, 10)
;for
和 while
的使用場景,具體能夠看這個問題的答案:Array.isArray()
,Object.prototype.toString.call()
,typeOf
, instanceOf
來檢查 arr
是否是數組類型,默認 arr
就是數組類型;( ? : )
來減小 if
的嵌套,提升代碼可讀性;++
)和自減(--
)運算符使用 +=
和 -=
代替 (for
中的最後一個條件除外);ES6
中的默認參數方式(快速排序中)簡化代碼,將關鍵邏輯突出;Eslint
+ Airbnb 全局控制代碼風格;function
聲明函數,具體緣由見:從 shuffle 看代碼品味。這是個人品味,你的呢:)