做者:Abhilash Kakumanu翻譯:瘋狂的技術宅javascript
原文:https://stackabuse.com/quicks...前端
未經容許嚴禁轉載java
排序是指以特定順序(數字或字母)排列線性表的元素。排序一般與搜索一塊兒配合使用。程序員
有許多排序算法,而迄今爲止最快的算法之一是快速排序(Quicksort)。面試
快速排序用分治策略對給定的列表元素進行排序。這意味着算法將問題分解爲子問題,直到子問題變得足夠簡單能夠直接解決爲止。算法
從算法上講,這能夠用遞歸或循環實現。可是對於這個問題,用遞歸法更爲天然。segmentfault
先看一下快速排序的工做原理:數組
接下來經過一個例子理解這些步驟。假設有一個含有未排序元素 [7, -2, 4, 1, 6, 5, 0, -4, 2]
的數組。選擇最後一個元素做爲基準。數組的分解步驟以下圖所示:服務器
在算法的步驟1中被選爲基準的元素帶顏色。分區後,基準元素始終處於數組中的正確位置。微信
黑色粗體邊框的數組表示該特定遞歸分支結束時的樣子,最後獲得的數組只包含一個元素。
最後能夠看到該算法的結果排序。
這一算法的主幹是「分區」步驟。不管用遞歸仍是循環的方法,這個步驟都是同樣的。
正是由於這個特色,首先編寫爲數組分區的代碼 partition()
:
function partition(arr, start, end){ // 以最後一個元素爲基準 const pivotValue = arr[end]; let pivotIndex = start; for (let i = start; i < end; i++) { if (arr[i] < pivotValue) { // 交換元素 [arr[i], arr[pivotIndex]] = [arr[pivotIndex], arr[i]]; // 移動到下一個元素 pivotIndex++; } } // 把基準值放在中間 [arr[pivotIndex], arr[end]] = [arr[end], arr[pivotIndex]] return pivotIndex; };
代碼以最後一個元素爲基準,用變量 pivotIndex
來跟蹤「中間」位置,這個位置左側的全部元素都比 pivotValue
小,而右側的元素都比 pivotValue
大。
最後一步把基準(最後一個元素)與 pivotIndex
交換。
在實現了 partition()
函數以後,咱們必須遞歸地解決這個問題,並應用分區邏輯以完成其他步驟:
function quickSortRecursive(arr, start, end) { // 終止條件 if (start >= end) { return; } // 返回 pivotIndex let index = partition(arr, start, end); // 將相同的邏輯遞歸地用於左右子數組 quickSort(arr, start, index - 1); quickSort(arr, index + 1, end); }
在這個函數中首先對數組進行分區,以後對左右兩個子數組進行分區。只要這個函數收到一個不爲空或有多個元素的數組,則將重複該過程。
空數組和僅包含一個元素的數組被視爲已排序。
最後用下面的例子進行測試:
array = [7, -2, 4, 1, 6, 5, 0, -4, 2] quickSortRecursive(array, 0, array.length - 1) console.log(array)
輸出:
-4,-2,0,1,2,4,5,6,7
快速排序的遞歸方法更加直觀。可是用循環實現快速排序是一個相對常見的面試題。
與大多數的遞歸到循環的轉換方案同樣,最早想到的是用棧來模擬遞歸調用。這樣作能夠重用一些咱們熟悉的遞歸邏輯,並在循環中使用。
咱們須要一種跟蹤剩下的未排序子數組的方法。一種方法是簡單地把「成對」的元素保留在堆棧中,用來表示給定未排序子數組的 start
和 end
。
JavaScript 沒有顯式的棧數據結構,可是數組支持 push()
和 pop()
函數。可是不支持 peek()
函數,因此必須用 stack [stack.length-1]
手動檢查棧頂。
咱們將使用與遞歸方法相同的「分區」功能。看看如何編寫Quicksort部分:
function quickSortIterative(arr) { // 用push()和pop()函數建立一個將做爲棧使用的數組 stack = []; // 將整個初始數組作爲「未排序的子數組」 stack.push(0); stack.push(arr.length - 1); // 沒有顯式的peek()函數 // 只要存在未排序的子數組,就重複循環 while(stack[stack.length - 1] >= 0){ // 提取頂部未排序的子數組 end = stack.pop(); start = stack.pop(); pivotIndex = partition(arr, start, end); // 若是基準的左側有未排序的元素, // 則將該子數組添加到棧中,以便稍後對其進行排序 if (pivotIndex - 1 > start){ stack.push(start); stack.push(pivotIndex - 1); } // 若是基準的右側有未排序的元素, // 則將該子數組添加到棧中,以便稍後對其進行排序 if (pivotIndex + 1 < end){ stack.push(pivotIndex + 1); stack.push(end); } } }
如下是測試代碼:
ourArray = [7, -2, 4, 1, 6, 5, 0, -4, 2] quickSortIterative(ourArray) console.log(ourArray)
輸出:
-4,-2,0,1,2,4,5,6,7
當涉及到排序算法時,將其可視化能幫咱們直觀的瞭解它們是怎樣運做的,下面這個例子搬運自維基百科:
在圖中也把最後一個元素做爲基準。給定數組分區後,遞歸遍歷左側,直到將其徹底排序爲止。而後對右側進行排序。
如今討論它的時間和空間複雜度。快速排序在最壞狀況下的時間複雜度是 $O(n^2)$。平均時間複雜度爲 $O(n\log n)$。一般,使用隨機版本的快速排序能夠避免最壞的狀況。
快速排序算法的弱點是基準的選擇。每選擇一次錯誤的基準(大於或小於大多數元素的基準)都會帶來最壞的時間複雜度。在重複選擇基準時,若是元素值小於或大於該元素的基準時,時間複雜度爲 $O(n\log n)$。
根據經驗能夠觀察到,不管採用哪一種數據基準選擇策略,快速排序的時間複雜度都傾向於具備 $O(n\log n)$ 。
快速排序不會佔用任何額外的空間(不包括爲遞歸調用保留的空間)。這種算法被稱爲in-place算法,不須要額外的空間。