用 JS 實現各類經典排序算法

  最近在複習算法相關的知識點,總結了一下以供參考,也是對本身知識的一種複習。排序算法的幾個主要指標是,時間複雜度(平均、最好、最差)、空間複雜度和穩定性。本文主要描述幾種常見算法:簡單選擇排序、冒泡排序、簡單插入排序、希爾排序、歸併排序、快速排序,還有它們的指標統計。算法的實現都基於JS實現的。
  
排序算法主要指標總結
算法名稱 平均時間複雜度 最好時間複雜度 最差時間複雜度 空間複雜度 穩定性
冒泡排序  O(n2) O(n)    O(n2 O(1)  穩定 
選擇排序 O(n2 O(n2  O(n2)  O(1)   不穩定
插入排序 O(n2 O(n)   O(n2  O(1)   穩定
希爾排序 O(n1.3)  (不肯定) O(n)   O(n2)   O(1)    不穩定
快速排序 O(nlog2n)  O(nlog2n)  O(n2  O(log2n) 至O(n)  不穩定 
歸併排序  O(nlog2n)  O(nlog2n) O(nlog2n)   O(n)   穩定

 

1、冒泡排序算法

  冒泡排序的思路是:假設正在將一組數字按照升序排列,從第0個元素到第n-1個元素遍歷,若前面一個元素大於後面一個元素,則交換兩個元素,這樣可將整個序列中最大的元素冒泡到最後,而後再從第0個到第n-2遍歷,如此往復,直到完成。shell

  一、初級版:冒泡排序不管如何都要執行完兩重循環,故最好、最壞和平均時間複雜度均爲O(n2),不須要額外空間。冒泡排序是穩定的。express

function bubbleSort(array) {
  const n = array.length - 1
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n - i; j++) {
      if (array[j] > array[j + 1]) {
        const temp = array[j]
        array[j] = array[j + 1]
        array[j + 1] = temp
      }
    }
  }
return array }

  二、改進版:在內層循環以前設置一個標誌性變量pos,用於記錄每趟排序中最後一次進行交換的位置,因爲pos位置以後的記錄均已交換到位,故在進行下一趟排序是隻要掃描到pos位置。冒泡排序的最好時間複雜度能夠提高到O(n)。(並不意味着實際應用中改進版比初級版快,只是最好狀況下快)數組

function bubbleSort(array) {
  let i = array.length - 1;
  while (i > 0) {
    let pos = 0;
    for (let j = 0; j < i; j++) {
      if (array[j] > array[j + 1]) {
        pos = j;
        let temp = array[j];
        array[j] = array[j + 1];
        array[j + 1] = temp;
      }
    }
    i = pos;
  }
  return array;
}

2、選擇排序數據結構

  選擇排序的思路是:從所有序列中選取最小的,與第0個元素交換,而後從下一個元素日後找出最小的,與該元素元素交換,以此類推,直到選取最後一個元素。由於不管如何都要完整地執行內外兩重循環,故最好、最差和平均時間複雜度都是O(n2),惟一的好處就是不佔用額外的內存空間。選擇排序是不穩定的。less

function selectionSort(array) {
  let length = array.length;
  for (let i = 0; i < length - 1; i++) {
    let min = i;
    for (let j = i + 1; j < length; j++) {
      if (array[j] < array[min]) {
        min = j;
      }
    }
    let temp = array[i];
    array[i] = array[min];
    array[min] = temp;
  }
  return array;
}

3、簡單插入排序ui

  簡單插入排序思路是相似撲克牌的排序,每次從未排序序列的第一個元素,插入到已排序序列中的合適位置。編碼

  一、初級版:經過兩重循環,最差和平均時間複雜度爲O(n2),最好狀況是原序列已有序,則忽略內層循環,時間複雜度O(n)。插入排序是穩定的。spa

function insertionSort(array) {
  for (let i = 1; i < array.length; i++) {
    let key = array[i];
    let j = i - 1;
    while (j >= 0 && array[j] > key) {
      array[j + 1] = array[j];
      j--;
    }
    array[j + 1] = key;
  }
  return array;
}

  二、改進版:二分查找改進的插入排序code

function insertionSort(array) {
  for (let i = 1; i < array.length; i++) {
    let key = array[i], left = 0, right = i - 1;
    while (left < right) {
      let middle = Math.floor((left + right) / 2);
      if (key < array[middle]) {
        right = middle - 1;
      } else {
        left = middle;
      }
    }
    for (let j = i - 1; j >= left; j--) {
      array[j + 1] = array[j];
    }
    array[left] = key;
  }
  return array;
}

4、希爾排序

  希爾排序的思路是,先以一個較大的增量,將序列分紅幾個子序列,將這幾個子序列分別排序後,合併,在縮小增量進行一樣的操做,直到增量爲1時,序列已經基本有序,再進行簡單插入排序的效率就會較高。

  希爾排序的核心在於間隔序列的設定。既能夠提早設定好間隔序列,也能夠動態定義間隔序列。

  一、動態間隔序列希爾排序(能夠設置間隔區間)

function shellSort(array) {
  const N = array.length
  let gap = 1;
  while (gap < N / 3) {
    gap = gap * 3 + 1;
  }
  while (gap >= 1) {
    for (let i = gap; i < N; i++) {
      for (let j = i; j >= gap && array[j] > array[j - gap]; j -= gap) {
        let tmp = array[j]
        array[j] = array[j - gap]
        array[j - gap] = tmp
      }
    }
    gap = (gap - 1) / 3
  }
  return array
}

  二、硬編碼間隔序列的希爾排序(gaps 是 [10,4,1] 這樣設定好間隔的數組)

function shellSort1(array, gaps) {
  for (let g = 0; g < gaps.length; g++) {
    const gap = gaps[g]
    for (let i = gap; i < array.length; i++) {
      for (j = i; j >= gap && array[j] > array[j - gap]; j -= gap) {
        let tmp = array[j]
        array[j] = array[j - gap]
        array[j - gap] = tmp
      }
    }
  }
}

 5、快速排序

  快速排序的思想是,選取第一個數爲基準,經過一次遍歷將小於它的元素放到它的左側,將大於它的元素放到它的右側,而後對它的左右兩個子序列分別遞歸地執行一樣的操做。

function quickSort (array) {
  if (array.length <= 1) {
    return []
  }
  const lesser = [];
  const greater = [];
  const pivot = array[0];
  for (let i = 1; i < array.length; i++) {
    if (array[i] < pivot) {
      lesser.push(array[i]);
    } else {
      greater.push(array[i]);
    }
  }
  return quickSort(lesser).concat(pivot, quickSort(greater));
}

6、歸併排序

  歸併排序的思路是,利用二分的特性,將序列分紅兩個子序列進行排序,將排序後的兩個子序列歸併(合併),當序列的長度爲2時,它的兩個子序列長度爲1,即視爲有序,可直接合並,即達到歸併排序的最小子狀態。

  一、初級版(效率低)

function mergeSort(array) {
  if (array.length < 2) {
    return array;
  }
  const middle = parseInt(array.length / 2);
  const left = array.slice(0, middle);
  const right = array.slice(middle);
  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  const newArray = [];
  while (left.length && right.length) {
    if (left[0] <= right[0]) {
      newArray.push(left.shift());
    } else {
      newArray.push(right.shift());
    }
  }
  while (left.length) {
    newArray.push(left.shift());
  }
  while (right.length) {
    newArray.push(right.shift());
  }
  return newArray;
}

   二、改進版(效率較高)

function mergeSort(array = [], start = 0, length) {
  length = length || array.length;
  if (length - start < 2) {
    return;
  }
  const mid = Math.floor((start + length) / 2);
  mergeSort(array, start, mid);
  mergeSort(array, mid, length);
  merge(array, start, mid, length);
  return array;
}
function merge(array, start, mid, length) {
  const left = array.slice(start, mid);
  const right = array.slice(mid, length);
  left.push(Number.MAX_SAFE_INTEGER);
  right.push(Number.MAX_SAFE_INTEGER);
  for (let i = start, j = 0, k = 0; i < length; i++) {
    if (left[j] < right[k]) {
      array[i] = left[j];
      j++;
    } else {
      array[i] = right[k];
      k++;
    }
  }
}

  三、高級版(效率高)

function mergeSort2(array = []) {
if (array.length < 2) { return; } let step = 1; let left; while (step < array.length) { left = 0;while (left + step * 2 <= array.length) { merge2(array, left, left + step, left + step * 2); left = left + step * 2; } if (left + step < array.length) { merge2(array, left, left + step, array.length); } step *= 5; } return array } function merge2(array, start, mid, length) { const rightArr = new Array(length - mid + 1); const leftArr = new Array(mid - start + 1); let k = mid; for (let i = 0; i < length - mid; i++) { rightArr[i] = array[k]; k++; } k = start; for (let i = 0; i < mid - start; i++) { leftArr[i] = array[k]; k++; } rightArr[length - mid] = Infinity; leftArr[mid - start] = Infinity; let m = 0; let n = 0; for (k = startLeft; k < stopRight; ++k) { if (leftArr[m] <= rightArr[n]) { array[k] = leftArr[m]; m++; } else { array[k] = rightArr[n]; n++; } } }

   歸併高級版比改進版效率高的緣由有兩個:

 一、算法遞歸次數少;

 二、新建Array,對其賦值效率比Array.slice()高

 

對比1000萬的數組排序所用的時間

希爾排序時間爲:18295ms
快速排序時間爲:9754ms
改進版歸併算法時間爲:3677ms
高級版歸併算法2時間爲:1252ms
相關文章
相關標籤/搜索