【30分鐘學會】用js玩點算法(1):排序基礎

前言

前端工程師因爲業務特色比較少接觸算法的東西,因此本系列也不會講太過深刻的東西,更多的是做爲知識擴展和思惟邏輯的培養。
排序就是將一組對象按照某種邏輯順序從新排列的過程,本篇將介紹幾種金典的排序算法。javascript

在計算時代早期,你們廣泛認爲30%的計算週期都用在了排序上。若是今天這個比例下降了,可能的緣由之一是現在的排序算法更加高效,而並不是排序的重要性下降了。

約定都是從小到大排序,當前項爲i。swap是交換數組內位置的函數,實現以下:前端

function swap(_arr, index1, index2) {
  const arr = _arr;
  arr[index1] += arr[index2];
  arr[index2] = arr[index1] - arr[index2];
  arr[index1] -= arr[index2];
}

冒泡排序

學校裏第一個學的排序方式老是冒泡排序,雖然它效率低,但最容易理解。冒泡排序比較任何兩個相鄰的項,若是第一個比第二個大,則交換它們。元素項向上移動至正確的順序,就好像氣泡升至表面同樣,冒泡排序所以得名。java

通常方案

基本思路:算法

  1. 前一項(i)與後一項(i+1)項比較,若是前一項比後一項大就交換這兩項;
  2. 重複這個過程到最後;
  3. 一趟完成後再從頭開始重複上面的步驟,有多少項就要重複幾回。

代碼實現:數組

function bubbleSort(_arr) {
  const arr = [].slice.call(_arr);
  const len = arr.length;
  for (let i = 0; i < len; i += 1) {
    for (let f = 0; f < len - 1; f += 1) {
      if (arr[f] > arr[f + 1]) {
        swap(arr, f, f + 1);
      }
    }
  }
  return arr;
}

示例過程:緩存

// 初始
5 4 9 5 3

// 第一趟
4 5 9 5 3  // 5>4,交換
^ ^
4 5 9 5 3  // 5<9,不變
  ^ ^
4 5 5 9 3  // 9>5,交換
    ^ ^
4 5 5 3 9  // 9>3,交換
      ^ ^

// 第二趟
4 5 5 3 9  // 4<5,不變
^ ^
4 5 5 3 9  // 5=5,不變
  ^ ^
4 5 3 5 9  // 5>3,交換
    ^ ^
4 5 3 5 9  // 5<9,不變
      ^ ^

// 第三趟
4 5 3 5 9  // 4<5,不變
^ ^
4 3 5 5 9  // 5>3,交換
  ^ ^
4 3 5 5 9  // 5=5,不變
    ^ ^
4 3 5 5 9  // 5<9,不變
      ^ ^

// 第四趟
3 4 5 5 9  // 4>3,交換
^ ^
3 4 5 5 9  // 4<5,不變
  ^ ^
3 4 5 5 9  // 5=5,不變
    ^ ^
3 4 5 5 9  // 5<9,不變
      ^ ^

// 第五趟
3 4 5 5 9  // 3<4,不變
^ ^
3 4 5 5 9  // 4<5,不變
  ^ ^
3 4 5 5 9  // 5=5,不變
    ^ ^
3 4 5 5 9  // 5<9,不變
      ^ ^

// 結果
3 4 5 5 9

改進方案

經過上面的排序過程,能夠發現其實每一趟就能夠肯定最後一位的位置了,因此能夠不用再比較最後的位置。代碼改造也很小,只要在內循環減去已經肯定的位置數便可。前端工程師

function modifiedBubbleSort(_arr) {
  const arr = [].slice.call(_arr);
  const len = arr.length;
  for (let i = 0; i < len; i += 1) {
    for (let f = 0; f < len - i - 1; f += 1) {
      if (arr[f] > arr[f + 1]) {
        swap(arr, f, f + 1);
      }
    }
  }
  return arr;
}

示例過程:dom

// 初始
5 4 9 5 3

// 第一趟
4 5 9 5 3  // 5>4,交換
^ ^
4 5 9 5 3  // 5<9,不變
  ^ ^
4 5 5 9 3  // 9>5,交換
    ^ ^
4 5 5 3 9  // 9>3,交換
      ^ ^

// 第二趟
4 5 5 3 9  // 4<5,不變
^ ^
4 5 5 3 9  // 5=5,不變
  ^ ^
4 5 3 5 9  // 5>3,交換
    ^ ^

// 第三趟
4 5 3 5 9  // 4<5,不變
^ ^
4 3 5 5 9  // 5>3,交換
  ^ ^

// 第四趟
3 4 5 5 9  // 4>3,交換
^ ^

// 結果
3 4 5 5 9

選擇排序

選擇排序算法是一種原址比較排序算法。這也是比較簡單的過程,只要不斷遍歷找到最小的數依次放入位置便可。
基本思路:函數

  1. 設定一個指針指向最小的數,從0號位開始;
  2. 遍歷數據,若是遇到比當前指針指向的數還小的數,就將指針從新指向這個新位置;
  3. 遍歷完成即獲得了最小的數的位置,把0號位與這個位置的數交換;
  4. 接下來就是1號位,重複以上步驟直到所有位置都正確。

代碼實現:性能

function selectionSort(_arr) {
  const arr = [].slice.call(_arr);
  const len = arr.length;
  for (let i = 0; i < len - 1; i += 1) {
    let indexMin = i;
    for (let f = i + 1; f < len; f += 1) {
      if (arr[indexMin] > arr[f]) {
        indexMin = f;
      }
    }
    if (indexMin !== i) {
      swap(arr, indexMin, i);
    }
  }
  return arr;
}

示例過程:

// 初始
5 4 9 5 3

// 第一趟,指針指向0號位
5 4 9 5 3  // 4<5,指針指向1號位
  ^
5 4 9 5 3  // 9>4,指針不變
  ^
5 4 9 5 3  // 5>4,指針不變
  ^
5 4 9 5 3  // 3<4,指針指向4號位
        ^
3 4 9 5 5   // 遍歷結束,交換0號位和4號位

// 第二趟,指針指向1號位
3 4 9 5 5  // 9>4,指針不變
  ^
3 4 9 5 5  // 5>4,指針不變
  ^
3 4 9 5 5  // 5>4,指針不變
  ^
3 4 9 5 5  // 遍歷結束,1號位不變

// 第三趟,指針指向2號位
3 4 9 5 5  // 5<9,指針指向3號位
      ^
3 4 9 5 5  // 5=5,指針不變
      ^
3 4 5 9 5  // 遍歷結束,交換2號位和3號位

// 第四趟,指針指向3號位
3 4 5 9 5  // 5<9,指針指向4號位
        ^
3 4 5 5 9  // 遍歷結束,交換3號位和4號位

// 結果
3 4 5 5 9

插入排序

插入排序就是要把後面的數往前面插入。假定第一項已經排序了,接着從第二項開始,依次判斷當前項應該插入到前面的哪一個位置。
基本思路:

  1. 從第二項開始(i=1),當前項(i),緩存其值和位置;
  2. 向前遍歷,指針f初始化爲i位置,若是f-1大於當前項的值,則交換f和f-1(即f-1向後移動一位),並f--;
  3. 若是遇到f-1小於當前值,或f=0時中止循環,這時候f便是當前項的位置,將以前的緩存值寫入該位置。

代碼實現:

function insertionSort(_arr) {
  const arr = [].slice.call(_arr);
  const len = arr.length;
  for (let i = 1; i < len; i += 1) {
    let f = i;
    const temp = arr[i];
    while (f > 0 && arr[f - 1] > temp) {
      arr[f] = arr[f - 1];
      f -= 1;
    }
    arr[f] = temp;
  }
  return arr;
}

示例過程:

// 初始
5 4 9 5 3

// 第一趟,當前項是1號位,數字4
_ 5 9 5 3  // 4<5,5向後移動
^ ^
4 5 9 5 3  // 遍歷結束,寫入4
^

// 第二趟,當前項是2號位,數字9
4 5 9 5 3  // 9>5,不變
  ^
4 5 9 5 3  // 9>4,不變,遍歷結束
^

// 第三趟,當前項是3號位,數字5
4 5 _ 9 3  // 5<9,9向後移動
    ^ ^
4 5 _ 9 3  // 5=5,不變
  ^
4 5 _ 9 3  // 5>4,不變
^
4 5 5 9 3  // 遍歷結束,寫入5
    ^

// 第四趟,當前項是4號位,數字3
4 5 5 _ 9  // 3<9,9向後移動
      ^ ^
4 5 _ 5 9  // 3<5,5向後移動
    ^ ^
4 _ 5 5 9  // 3<5,5向後移動
  ^ ^
_ 4 5 5 9  // 3<4,4向後移動
^ ^
3 4 5 5 9  // 遍歷結束,寫入3
^

// 結果
3 4 5 5 9

歸併排序

歸併排序是一種分治算法。其思想是將原始數組切分紅較小的數組,直到每一個小數組只有一個位置,接着將小數組歸併成較大的數組,直到最後只有一個排序完畢的大數組。
基本思路:

  1. 將數組從中間切成兩個數組;
  2. 若是切出來的數組長度不爲1,則重複上一步,直到全部切分出來的數組的長度都爲1;
  3. 以從小到大的順序合併小數組,先是兩個長度爲1的數組合併成長度爲2的數組;
  4. 再是兩個長度爲2的數組合併爲長度爲4的數組,以此類推。

代碼實現:

function mergeSort(_arr) {
  const arr = [].slice.call(_arr);
  function merge(left, right) {
    const result = [];
    let iL = 0;
    let iR = 0;
    const lenL = left.length;
    const lenR = right.length;
    while (iL < lenL && iR < lenR) {
      if (left[iL] < right[iR]) {
        result.push(left[iL]);
        iL += 1;
      } else {
        result.push(right[iR]);
        iR += 1;
      }
    }
    while (iL < lenL) {
      result.push(left[iL]);
      iL += 1;
    }
    while (iR < lenR) {
      result.push(right[iR]);
      iR += 1;
    }
    return result;
  }
  return (function cut(_array) {
    const len = _array.length;
    if (len === 1) {
      return _array;
    }
    const mid = Math.floor(len / 2);
    const left = _array.slice(0, mid);
    const right = _array.slice(mid, len);
    return merge(cut(left), cut(right));
  }(arr));
}

示例過程:

// 初始
5 4 9 5 3

// 切分
[5 4] [9 5 3]  // 中間數是9
     ^
([5] [4]) [9 5 3]  // 進入左側數組,中間數是4
    ^
([5] [4]) ([9] [5 3])  // 左側切分完,進入右側數組,中間數是5
              ^
([5] [4]) ([9] ([5] [3]))  // 左側切分完,進入右側數組,中間數是3
                   ^

// 合併[5]和[3]
([5] [4]) ([9] [3 $])  // 3<5,入3
                ^
([5] [4]) ([9] [3 5])  // 入5,完畢
                  ^

// 合併[9]和[3 5]
([5] [4]) [3 $ $]  // 3<9,入3
           ^
([5] [4]) [3 5 $]  // 5<9,入5
             ^
([5] [4]) [3 5 9]  // 入9,完畢
               ^

// 合併[5]和[4]
[4 $] [3 5 9]  // 4<5,入4
 ^
[4 5] [3 5 9]  // 入5,完畢
   ^

// 合併[4 5]和[3 5 9]
[3 $ $ $ $]  // 4>3,入3
 ^
[3 4 $ $ $]  // 4<5,入4
   ^
[3 4 5 $ $]  // 5=5,入5
     ^
[3 4 5 5 $]  // 入5
       ^
[3 4 5 5 9]  // 入9,完畢
         ^

// 結果
3 4 5 5 9

快速排序

快速排序的思想跟歸併很像,都是分治方法,但它沒有像歸併排序那樣將它們分割開,而是使用指針遊標來標記,每次會肯定一個主元的位置。稍微會比前面的複雜一些。
基本思路:

  1. 取數組的第0項做爲主元,緩存0號位的數。
  2. 設定一個從0號位開始的low指針,一個從末尾開始的high指針;
  3. 先從high指針開始移動,指針指向的數與主元作比較,若是大於或等於主元則繼續向前移動,若是小於主元則停下並把high指針指向的數替換到當前low指針指向的位置;
  4. 再從low指針開始移動,指針指向的數與主元作比較,若是小於或等於主元則繼續向後移動,若是大於主元則停下並把low指針指向的數替換到當前high指針指向的位置;
  5. 如此循環交替移動兩個指針,直到low指針的指向位高於或等於high的指向位;
  6. 至此low指向位便是主元的位置pivotloc,將主元寫入low指向的位置;
  7. 以此位置pivotloc爲分割,在左右兩邊重複上述的步驟,直到排序完成。

代碼實現:

function quickSort(_arr) {
  const arr = [].slice.call(_arr);
  function partition(low, high) {
    const pivotkey = arr[low];
    let i = low;
    let j = high;
    while (i < j) {
      while (i < j && arr[j] >= pivotkey) {
        j -= 1;
      }
      arr[i] = arr[j];
      while (i < j && arr[i] <= pivotkey) {
        i += 1;
      }
      arr[j] = arr[i];
    }
    arr[i] = pivotkey;
    return i;
  }
  (function QSort(low, high) {
    if (low < high) {
      const pivotloc = partition(low, high);
      QSort(low, pivotloc - 1);
      QSort(pivotloc + 1, high);
    }
  }(0, arr.length - 1));
  return arr;
}

示例過程:

// 初始
5 4 9 5 3

// 第一趟,主元爲5
5 4 9 5 3  // high開始移動,3<5,high中止
^L      ^H
3 4 9 5 3  // 將high指向數3寫入到low位置
^L      ^H
3 4 9 5 3  // low開始移動,3<5,繼續前進
^L      ^H
3 4 9 5 3  // 4<5,繼續前進
  ^L    ^H
3 4 9 5 3  // 9>5,low中止
    ^L  ^H
3 4 9 5 9  // 將low指向數9寫入到high位置
    ^L  ^H
3 4 9 5 9  // high開始移動,9>5,繼續後退
    ^L  ^H
3 4 9 5 9  // high開始移動,5=5,繼續後退
    ^L^H
3 4 5 5 9  // 兩指針重合,結束,肯定主元5的位置,寫入
    *

// 第二趟,主元爲3
3 4 5 5 9  // high開始移動,4>3,繼續後退
^L^H*
3 4 5 5 9  // 兩指針重合,結束,肯定主元3的位置,寫入
*   *

// 第三趟,主元爲4
3 4 5 5 9  // 兩指針重合,結束,肯定主元4的位置,寫入
* * *

// 第四趟,主元爲5
3 4 5 5 9  // high開始移動,9>5,繼續後退
* * * ^L^H
3 4 5 5 9  // 兩指針重合,結束,肯定主元5的位置,寫入
* * * *

// 第五趟,主元爲9
3 4 5 5 9  // 兩指針重合,結束,肯定主元9的位置,寫入
* * * * *

// 結果
3 4 5 5 9

簡易性能測試

上述的這麼多種排序算法哪一個比較快?這是咱們比較好奇的問題,咱們隨機生成10000個數據來測試一下吧。
兩個輔助函數:getRandomArray用來生成隨機數的數組,costClock用來統計耗時。

function getRandomArray(len = 10000, min = 0, max = 100) {
  const array = [];
  const w = max - min;
  for (let i = 0; i < len; i += 1) {
    array.push(parseInt((Math.random() * w) + min, 10));
  }
  return array;
}

function costClock(fn) {
  const now = new Date().getTime();
  const data = fn();
  const pass = new Date().getTime() - now;
  return {
    data,
    cost: pass,
  };
}

測試用例以下:

const array = getRandomArray(10000);
const result1 = costClock(() => bubbleSort(array));
const result2 = costClock(() => modifiedBubbleSort(array));
const result3 = costClock(() => selectionSort(array));
const result4 = costClock(() => insertionSort(array));
const result5 = costClock(() => mergeSort(array));
const result6 = costClock(() => quickSort(array));
console.log(result1);
console.log(result2);
console.log(result3);
console.log(result4);
console.log(result5);
console.log(result6);

結果以下圖,可見快速排序不愧是快速排序,不須要交互數據以及分治方法是其高效的主要緣由。

結果圖

相關文章
相關標籤/搜索