1. 程式人生 > >快速排序的JavaScript實現

快速排序的JavaScript實現

數據結構和算法 超過 大於 接下來 index tro 其他 div 延伸

思想

分治的思想,將原始數組分為較小的數組(但沒有像歸並排序一樣將它們分隔開)。

  1. 主元選擇: 從數組中任意選擇一項作為主元,通常為數組的第一項,即arr[i];或數組的中間項, arr[Math.floor((i+j)/2)];
  2. 劃分操作: 創建兩個指針,左邊一個指向數組的第一項,右邊一個指向數組的最後一項。向右移動左指針,直到找到一個不小於比主元的項;向左移動右指針,直到找到一個不大於主元的項。交換它們。然後重復這個過程,直到左指針超過了右指針。這樣使得比主元小的值都排在主元之前,而比主元大的值都排在主元之後。註意:在這個過程中,主元的位置也可能發生了改變;而主元本身在一次劃分操作之後不會在正確的位置,其正確的位置應該在本次劃分操作後最終得到的那個分割點上(即sliceIndex上),在下輪操作的右半路操作會立刻把主元換到正確的位置上
  3. 對子數組重復劃分操作: 比主元小的項(即主元左邊的部分)組成一個子數組,比主元大的項(即主元右邊的部分)組成另一個子數組。對這兩個小數組繼續執行主元選擇和劃分操作。直到數組已完全排序。

代碼

代碼段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實現