1. 程式人生 > 實用技巧 >js中常見排序方法詳細講解

js中常見排序方法詳細講解

一、 大O表示法

在進行排序演算法之前,我們得先掌握一種對演算法效率的表示方法,大O表示法。

我們使用大O表示法來表示演算法的時間複雜度,是用來表示演算法的速度隨著資料量的變化而如何變化的。

常見的大O表示函式:

  • O(1):常數的
  • O(log n):對數的
  • O(n):線性的
  • O(n*log(n)):線性和對數乘積
  • O(n2):平方
  • O(2n):指數

推導大O表示法的方式:

  • 用 常量1取代執行時間中所有的加法常量
  • 在修改後的執行次數函式中,只保留最高階項
  • 如果最高存在且不為1,則去除與這個項相乘的常數

二、排序演算法

排序演算法有很多很多,這裡就不全部實現,只實現一些比較常見的排序演算法。

簡單排序:氣泡排序、選擇排序、插入排序
高階排序:希爾排序、快速排序

1.氣泡排序

氣泡排序演算法相對其他排序執行效率較低,但是它是最簡單的一種排序。

原理:氣泡排序的原理(以遞增序為例)是每次從頭開始依次比較相鄰的兩個元素,如果後面一個元素比前一個要小,說明順序不對,則將它們交換,本次迴圈完畢之後再次從頭開始掃描,直到某次掃描中沒有元素交換,說明每個元素都不比它後面的元素大,至此排序完成。

過程:

程式碼:

// 氣泡排序
function bubbleSort (array) {
  for(var i = 0; i < array.length - 1; i++) {         // 表示比較的趟數
for(var j = 0; j < array.length - i - 1; j++) { // 表示比較的次數 if(array[j] > array[j+1]) { var temp = array[j+1]; array[j+1] = array[j]; array[j] = temp; } } } } // 測試 var array = [3,24,7,1,2,10,6,12]; bubbleSort(array); console.log(array.toString());
// 結果為:1,2,3,6,7,10,12,24

效率:

  1. 氣泡排序的比較次數:
    通過大O表示法來推導過程,n個數字比較時,第一次迴圈比較n-1次,第二次迴圈比較n-2次,第三次比較n-3次,…,直到最後一次比較1次。
    比較次數:(n-1)+(n-2)+(n-3)+ … +3+2+1
    所以比較次數是:n*(n-1)/2
    根據大O表示法只保留最高階項,去除常量,只剩n*n
  2. 氣泡排序的交換次數:
    交換次數不是確定的,所以以每兩次比較交換一次來進行計算
    交換次數:n*(n-1)/4
    根據大O表示法只保留最高階項,去除常量,只剩n*n
因此氣泡排序的大O表示法為O(n2)

2.選擇排序

選擇排序可以看作是氣泡排序的改進版本。
思路:選擇排序,從頭至尾掃描序列,找出最小的一個元素,和第一個元素交換,接著從剩下的元素中繼續這種選擇和交換方式,最終得到一個有序序列。
過程:

程式碼:

// 選擇排序
function selectionSort(array) {
  var min;
  for(var i = 0; i < array.length - 1; i++) {   // 外層迴圈,從0位置開始取資料
    min = i;
    for(var j = i + 1; j < array.length; j++) { // 內層迴圈,從i+1位置開始選擇出最小的資料
      if(array[min] > array[j]) {
        min = j;              // 找到小的資料,並且對min賦值
      }
    }
    var temp = array[min];    // 此時最小的資料就是第min個位置的資料,min和i位置資料交換
    array[min] = array[i];
    array[i] = temp;
  }
}
// 測試
var array = [4,8,2,7,3,1];
selectionSort(array);
console.log(array.toString());
// 結果為:1,2,3,4,7,8

效率:

  1. 選擇排序的比較次數:
    選擇排序和氣泡排序的比較次數都是:n*(n-1)/2
    用大O表示法都是O(n2)
  2. 選擇排序的交換次數:
    選擇排序每次進行選擇時,最多需要交換1次,一共遍歷n-1次
    用大O表示法就是O(n)
    所以選擇排序通常認為在執行效率上是高於氣泡排序的

3.插入排序

插入排序是簡單排序中效率最好的一種,而且插入排序也是學習其他高階排序的基礎。
思路:
將待排序序列中第一個元素看做一個有序序列,把第二個元素到最後一個元素當成是未排序序列,從頭到尾依次掃描未排序序列,將掃描到的每個元素插入有序序列的適當位置。
過程:

程式碼:

// 插入排序
function insertionSort(array) {
  for(var i = 1; i < array.length; i++) {    // 外層迴圈,從第1個位置開始獲取資料,向前面區域性有序進行插入
    var temp = array[i]; // 取出要插入的元素
    var j = i; 
    while(array[j - 1] > temp && j > 0) {    // 內層迴圈,將要插入的元素和前面的元素依次進行比較,找到合適的插入位置
      array[j] = array[j-1];
      j--;
    }
    // 將temp放置到j位置上
    array[j] = temp;
  }
}

效率:

  1. 插入排序的比較次數:
    第一次最多需要比較1次,第二次最多需要比較2次,……,最後一次最多需要比較n-1次。
    因此插入排序的最多比較次數是1+2+3+…+n-1 = n*(n-1)/2
    但是每次插入我們大概只需要進行一半的比較
    所以插入排序的比較次數為n*(n-1)/4, 相對於冒泡和選擇排序,比較次數少了一半。
  2. 插入排序的複製次數:
    第一次最多需要複製1次,第二次最多需要複製2次,……,最後一次最多需要複製n-1次。
    因此插入排序的最多複製次數是n*(n-1)/2
    但是每次複製我們大概只需要進行一半的複製
    所以插入排序的複製次數為n*(n-1)/4

4.希爾排序

希爾排序是插入排序的一種高效的改進版,並且效率比插入排序更快。
插入排序的問題:
由於希爾排序基於插入排序,所以我們需要回顧一下插入排序以及它的問題。
插入排序的問題:
假設一個較小的資料項在很靠近右端的位置上,這裡本來應該時較大的資料項的位置,要把這個小資料項移動到左邊的正確位置,所有的中間資料項都必須向右移動一位,每個步驟對資料項都進行n次複製,平均下來就是移動n/2次,n個元素就是n*n/2= n2/2,所以插入排序的效率時O(n2)。
如果有某種方式,不需要一個個移動所有中間的資料項,就能把較小的資料項移動到左邊,那麼演算法的執行效率就會有很大的改進。

思路:
希爾排序就是把無序的陣列分割成很多的子序列,子序列不是逐段分割的,而是相隔特定增量的子序列,對各個子序列進行插入排序,然後再選擇一個更小的增量,將之前排序後的陣列按這個增量分割成多個子序列,對各個子序列進行插入排序,……,不斷選擇更小的增量,直到增量為1時,再對序列進行一次插入排序,使序列最終成為有序序列,即排序完成。

希爾排序增量的選擇:
初始增量為n/2,下一次的增量為原來的1/2,直到增量為1。
比如對於n = 100的陣列,增量間隔序列為:50,25,12,6,3,1
過程:

程式碼:

// 希爾排序
function shellSort(array) {
  // 獲取陣列的長度
  var length = array.length;
  // 初始化增量
  var gap = Math.floor(length / 2);
  // 迴圈讓gap不斷減小,直到為1
  while(gap >= 1) {
    // 以gap為間隔,進行分組,對分組進行插入排序
    for(var i = gap; i < length; i++) {
      var temp = array[i];
      var j = i;
      while(array[j-gap] > temp && j > gap - 1) {
        array[j] = array[j - gap]; 
        j -= gap;
      }
      // 將j位置的元素賦值temp
      array[j] = temp;
    }
    // 增量變化
    gap = Math.floor(gap / 2);
  }
}

// 測試:
var array = [62,88,58,47,61,35,73,51,99,37,93];
shellSort(array);
console.log(array.toString());
// 結果為:35,37,47,51,58,61,62,73,88,93,99

效率:
希爾排序的效率和增量是有關係的,但是,它的效率證明非常困難,甚至某些增量的效率至今沒有唄證明出來。但是經過統計,希爾排序使用原始增量,最壞情況下的的時間複雜度為O(n2),通常都要好於O(n2)。
總之,我們使用希爾排序大多數情況下效率都高於簡單排序。

5.快速排序

快速排序幾乎可以說是目前所有排序演算法中,最快的一種排序演算法。
快速排序其實是氣泡排序的升級版本,氣泡排序需要經過很多次交換,才能在一次迴圈中,將最大值放在正確的位置,而快速排序可以在一次迴圈中,找到某個元素的正確位置,並且該元素之後也不需要任何移動。
快速排序的思想就是分而治之。
思路:
快速排序採用分而治之的思想,將待排序的數中取出一個數作為基準數,將比這個數大的數字全部放在它的右邊,比這個數字小的或者等於的全部放在它的左邊,再對左右區間重複第二步,直到各個區間只有一個數。基準值我選擇陣列的第一項。
過程:

程式碼:

// 快排頂層呼叫函式
function quickSort(arr) { 
  quick(0, arr.length - 1, arr);
}
// 快排遞迴呼叫函式
function quick(left, right, arr) {
  var i = left + 1;  // 指標i從left+1向右找比基準值大的,因為基準值是arr[left],基準值自己和自己不做比較
  var j = right; // 指標j從right向左找比基準值小的
  var base = arr[left];  // 基準值選擇待排序陣列的最左一項
  if(arr.length <= 1) return;  // 如果陣列為空或只有一個元素,則直接返回
  if(left >= right) return;   // 退出判斷,如果left >= right,本次遞迴退出
  while(i < j) {                        
    while(i < j && arr[j] > base) j--;   // 讓j指標一直向左找到小於基準值的
    while(i < j && arr[i] < base) i++;   // 讓i指標一直向右找到大於基準值的
    if(i < j) {                // 找到後,讓i,j指向的資料進行交換
      var temp = arr[i];
      arr[i] = arr[j];
      arr[j] = temp;
    }
  }                    
  arr[left] = arr[i];     // 直到i,j指標相遇,則交換i指向的資料和基準值的資料
  arr[i] = base;    // 這樣,就讓基準值的左邊資料都小於基準值,右邊資料都大於基準值
  quick(left, i-1, arr);    // 遞迴快排,left——i-1的資料
  quick(i+1, right, arr);   // 遞快排,i+1——right的資料    
};
var arr = [6, 3, 4, 7, 2, 9, 12, 1,8,22];
quickSort(arr);
console.log(arr.toString());
// 結果為:1,2,3,4,6,8,7,9,22,12

效率:
快速排序被認為是速度最快的排序演算法。
快速排序的平均速率是O(n * log n)