快速排序的JavaScript實現
阿新 • • 發佈:2018-06-24
數據結構和算法 超過 大於 接下來 index tro 其他 div 延伸
思想
分治的思想,將原始數組分為較小的數組(但沒有像歸並排序一樣將它們分隔開)。
- 主元選擇: 從數組中任意選擇一項作為主元,通常為數組的第一項,即arr[i];或數組的中間項, arr[Math.floor((i+j)/2)];
- 劃分操作: 創建兩個指針,左邊一個指向數組的第一項,右邊一個指向數組的最後一項。向右移動左指針,直到找到一個不小於比主元的項;向左移動右指針,直到找到一個不大於主元的項。交換它們。然後重復這個過程,直到左指針超過了右指針。這樣使得比主元小的值都排在主元之前,而比主元大的值都排在主元之後。註意:在這個過程中,主元的位置也可能發生了改變;而主元本身在一次劃分操作之後不會在正確的位置,其正確的位置應該在本次劃分操作後最終得到的那個分割點上(即sliceIndex上),在下輪操作的右半路操作會立刻把主元換到正確的位置上
- 對子數組重復劃分操作: 比主元小的項(即主元左邊的部分)組成一個子數組,比主元大的項(即主元右邊的部分)組成另一個子數組。對這兩個小數組繼續執行主元選擇和劃分操作。直到數組已完全排序。
代碼
代碼段1:
function quickSort(arr) {
return quick(arr, 0, arr.length-1);
}
function quick(arr, left, right) {
if(arr.length>1) {
const sliceIndex = partition(arr, left, right);
if (left < sliceIndex-1 ) {
quick(arr, left, sliceIndex-1);
}
if (sliceIndex < right) { //*1
quick(arr, sliceIndex, right);//*2
}
}
}
function partition(arr, left, right) {
let i = left;
let j = right;
//const pivot = arr[Math.floor((i+j)/2)];
const pivot = arr[i];
while(i< j) {
while (arr[i] < pivot) { // *3
//Q:*3,*4:為什麽這裏必須用<和>, 而不能用 <=或>= ?
//A:*3和*4必須是<和>,都不能包含=的情況——實際驗證結果也是如此
//因為如果包含=,那麽就永遠不處理pivot及值等於pivot的情況
i++;
}
while (arr[j] > pivot) { // *4
j--;
}
if(i<j) { // *5
//思考:為什麽這裏必須是 <= 而不能用 < ?
//A:它其實是為了下面一段的i++,j--。如果分開兩段寫,那麽這裏應該是i<j,而下面一段應該是i<=j
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
if(i<=j) {//*6
//這裏條件一定要是i<=j。
//如果條件僅僅是i<j,那麽當i==j的時候就永遠也進入不了這個條件,return 的值就是i也是j。那麽這一整段partion代碼下來,i可能從來沒有變過,又原封不動地return了i,即 const sliceIndex = partition(arr, left, right);的這個sliceIndex是和參數left相等的,那麽在接下來的quick(arr, sliceIndex, right)就等於之前的quick(arr,left,right),無限循環了。。。
//總之,這個條件的目的就是讓i一定要改變一次
i++;
j--;
}
}
return i;
}
思考
1. 其實每次劃分操作之後,主元並不在正確的位置上……那麽為什麽說每次劃分操作都把比主元小的值排在了主元之前,而把比主元大的值都排在了主元之後?
主元本身在一次劃分操作之後確實不在正確的位置,其正確的位置應該在本次劃分操作後最終得到的那個分割點上(即sliceIndex上),在下輪操作的右半路操作會立刻把主元換到正確的位置上。所以其實在本輪劃分操作最後,可以把主元交換到正確的位置上,再進行下輪操作,然後下輪操作就可以不管sliceIndex那個位置上了。
這種寫法其實是把小於主元的放到左邊,大於主元的放到右邊,等於主元的可能在左可能在右,而並不是把主元放在了正確的位置。
代碼改進成如下這樣就好理解了。
代碼段2:
function quickSort(arr) {
return quick(arr, 0, arr.length-1);
}
function quick(arr, left, right) {
if (arr.length ===1) {
return;
}
const sliceIndex = partition(arr, left, right);
if(left < sliceIndex-1) {
quick(arr, left, sliceIndex-1);
}
//下次劃分不用再考慮主元了
if(sliceIndex + 1 < right) { //*1
quick(arr, sliceIndex + 1, right);//*2
}
}
function partition(arr, left, right) {
let i = left;
let j = right;
const pivot = arr[i];
while(i < j) {
while(arr[i] < pivot) { //*3
i++;
}
while(arr[j] > pivot) {
j--;
}
if(i < j) {
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
if(i <= j) {
i++;
j--;
}
}
//交換主元到劃分位置上
const tempPivotIndex = arr.indexOf(pivot); //*7
arr[tempPivotIndex] = arr[i];//*8
arr[i]=pivot;//*9
return i;//其實也是等於j的
}
比起原代碼,這種寫法:把1,2做了修改,下次劃分操作不再考慮本次的slickIndex(即主元已經排好了);然後新增7,8,*9,就是在本次的劃分操作最後,把主元交換到了正確的位置上,這樣寫,與Es6寫法(代碼段3)的思路是一致的
工作過程
待畫圖
性能分析
- 時間復雜度: 最好、平均O(nlogn),最壞O(n^2)
- 空間復雜度: O(logn), 不穩定
- 特點:越有性能卻差
延伸:es6的實現
代碼段3:
function quickSortEs6(arr) {
if (!arr.length) {
//要處理的臨界條件一定是arr為空的情況,因為可能filter過濾後就一項也不剩了
//arr長度為1的條件可以不單獨處理,因為長度為1那麽[pivot, ...rest]=arr的rest就為[]了
return [];
}
const [pivot, ...rest] = arr;
return [
...quickSortEs6(rest.filter(item => item < pivot)),
pivot,
...quickSortEs6(rest.filter(item => item >= pivot)) //一定要有=不然和pivot相等的其他值會被過濾掉
]
}
這個比起之前的排法更好理解。
參考資料
-《學習JavaScript數據結構和算法》10.1.5
-《數據結構(C語言版)》9.3.2
快速排序的JavaScript實現