Jamie Chien

Jamie Chien • 2024-05-25

Javascript 排序演算法(Sorting Algorithm)

範例

前言

或多或少大家都有接觸過排序的問題,這篇文章想透過 Javascript 來實作並解釋幾個常見的排序演算法

目錄

泡沫排序法(Bubble Sort)

比較 array 中 2 個位置的值,透過 swap 交換 2 個值,並且 go through 整個 array 來確保整個 array 排序如預期

演算法較簡單,但效能較差,因此較少使用

實作 (Javascript)

const bubbleSort = (arr) => {
  for (let i = 0; i < arr.length - 2; i++) {
    for (let j = arr.length - 1; j >= i + 1; j--) {
      if (arr[j] < arr[j - 1]) {
        // swap
        const temp = arr[j];
        arr[j] = arr[j - 1];
        arr[j - 1] = temp;
      }
    }
  }

  return arr;
};

時間複雜度 (Time Complexity)

CaseComplexity
Worst CaseO(n^2)
Best CaseO(n)
Average CaseO(n^2)

插入排序法(Insertion Sort)

逐一從 array 取得值並且插入 array 最左邊(根據大小排序),因為可以確保左方 array 是排序好的,因此可以更快速的找到對應的位置並插入值

在實務上比 Bubble Sort 效率好一點

實作 (Javascript)

const insertionSort = (arr) => {
  for (let i = 1; i < arr.length; i++) {
    target = arr[i];
    let j = i - 1;
    while (j >= 0 && arr[j] > target) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = target;
  }

  return arr;
};

時間複雜度 (Time Complexity)

CaseComplexity
Worst CaseO(n^2)
Best CaseO(n)
Average CaseO(n^2)

選擇排序法(Selection Sort)

在 array 裡面還沒排序的部分找到最小的值,並把他放到已經排序的部分的最後面(和 array 裡面還沒排序的部分的第一個值交換)

插入排序法 相比,因為 選擇排序法 是從 unsorted array 中尋找最小值,所以他在最佳的情況下也是需要 O(n) 的時間複雜度

實作 (Javascript)

const selectionSort = (arr) => {
  for (let i = 0; i < arr.length - 1; i++) {
    // find the index of smallest value
    let smallestIndex = i;
    for (let j = i; j < arr.length; j++) {
      if (arr[j] < arr[smallestIndex]) {
        smallestIndex = j;
      }
    }
    // swap
    const temp = arr[i];
    arr[i] = arr[smallestIndex];
    arr[smallestIndex] = temp;
  }

  return arr;
};

時間複雜度 (Time Complexity)

CaseComplexity
Worst CaseO(n^2)
Best CaseO(n^2)
Average CaseO(n^2)

合併排序法(Merge Sort)

先將一個 array 拆成 n 個長度為 1 的小 array,接著兩兩比較並且組合,最後得到長度為 n 且已排序的 array (known as Divide and Conquer)

組合的方式是使用兩個 pointer 分別指向兩個要比較的 array 並從各自的 0 index 開始往後,因為可以確定這些 array 都是 sorted array,因此透過比較兩個 pointer 指向的值來決定哪個要先放到新 array 中,並讓 pointer++

會比較耗記憶體(空間複雜度較高)

Divide and Conquer 示意圖

實作 (Javascript)

// merge is used in mergeSort
const merge = (arr1, arr2) => {
  let p1 = 0;
  let p2 = 0;
  const result = [];

  while (p1 < arr1.length && p2 < arr2.length) {
    if (arr1[p1] < arr2[p2]) {
      // push arr1[p1] to result
      result.push(arr1[p1]);
      p1++;
    } else {
      // push arr2[p2] to result
      result.push(arr2[p2]);
      p2++;
    }
  }

  // if p1 not reach the end of the arr1
  while (p1 < arr1.length) {
    result.push(arr1[p1]);
    p1++;
  }

  // if p2 not react the end of the arr2
  while (p2 < arr2.length) {
    result.push(arr2[p2]);
    p2++;
  }

  return result;
};

const mergeSort = (arr) => {
  if (arr.length === 1) return arr;

  const middle = Math.floor(arr.length / 2);
  const leftArr = arr.slice(0, middle);
  const rightArr = arr.slice(middle, arr.length);
  return merge(mergeSort(leftArr), mergeSort(rightArr));
};

時間複雜度 (Time Complexity)

CaseComplexity
Worst CaseO(n log n)
Best CaseO(n log n)
Average CaseO(n log n)

堆積排序法(Heap Sort)

建立一個 Max Heap,因為可以確定 Max Heap 的 root node 一定會是最大值,因此把最右下角 leaf node 的值換到 root node 的位置,並重新建立一次 Max Heap 來取得下一個最大值,不斷循環這個步驟來達成目的

會透過一個 heapSize 來存目前 heap 的大小,這樣可以避免剛剛換完的最大值又被 heapify 到 root node

建立一個 Max Heap 需要不斷地把最大值換到 parent node 的位置,直到整個樹的 parent node 都大於他們各自的 child node,而將最大值教會到 parent node 的這個行為稱作 (Max) Heapify (Heapify 的時間複雜度為 O(log n))

限制條件:要能夠建立成 complete binary tree

heap sort 演示

實作 (Javascript)

const heapSort = (arr) => {
  let heapSize;

  const _swap = (i, j) => {
    const temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
  };

  const _maxHeapify = (parent) => {
    let largest;
    const leftChild = parent * 2 + 1;
    const rightChild = parent * 2 + 2;

    // Compare {parent} with {left child}
    if (leftChild <= heapSize && arr[leftChild] > arr[parent]) {
      largest = leftChild;
    } else {
      largest = parent;
    }

    // Compare {largest} with {right child}
    if (rightChild <= heapSize && arr[rightChild] > arr[largest]) {
      largest = rightChild;
    }

    // Do heapify down if largest is not parent
    if (largest !== parent) {
      // swap arr[parent] with arr[largest]
      _swap(parent, largest);
      // heapify down until to the right bottom leaf node
      _maxHeapify(largest);
    }
  };

  const _buildMaxHeap = () => {
    heapSize = arr.length - 1;

    for (let i = Math.floor(heapSize / 2); i >= 0; i--) {
      _maxHeapify(i);
    }
  };

  const sort = () => {
    _buildMaxHeap();

    for (let i = arr.length - 1; i >= 0; i--) {
      // exchange arr[0] with arr[i] (index i is the right bottom leaf node)
      _swap(0, i);
      // decrease the heapSize (to avoid heapify the max value got from prev step)
      heapSize--;
      // heapify from the root node
      _maxHeapify(0);
    }
  };

  sort();
  return arr;
};

時間複雜度 (Time Complexity)

CaseComplexity
Worst CaseO(n log n)
Best CaseO(n log n)
Average CaseO(n log n)

快速排序法(Quick Sort)

透過 Partition 演算法(選定一個 pivot,並將一個 array 拆分成兩個部分),接著再讓拆分的兩個部分再分別做 partition,就這樣遞回下去直到無法做 partition(長度小於 1)為止

透過 Partition 演算法 拆分的兩個部分都還不是 sorted array,但是可以確定選定的 pivot 是在這兩個部分的中間(sorted)

最常被用來做 sorting 的演算法

實作 (Javascript)

const quickSort = (arr) => {
  const _swap = (i, j) => {
    const temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
  };

  const _partition = (start, end) => {
    // The goal of partition is to find the position that suit for pivot value
    // use the last(end) value as pivot
    const pivotValue = arr[end];
    let pos = start - 1;

    for (let i = start; i <= end - 1; i++) {
      // swap pos value with the current value
      if (arr[i] <= pivotValue) {
        pos += 1;
        _swap(i, pos);
      }
    }
    // swap pos value with the pivot value
    _swap(pos + 1, end);

    return pos + 1;
  };

  const sort = (start, end) => {
    if (start < end) {
      const pos = _partition(start, end);
      sort(start, pos - 1);
      sort(pos + 1, end);
    }
  };

  sort(0, arr.length - 1);
  return arr;
};

時間複雜度 (Time Complexity)

CaseComplexity
Worst CaseO(n^2)
Best CaseO(n log n)
Average CaseO(n log n)

Worst Case: 如果今天要把已經從小到大排序好的 array 改成從大到小時(或是倒過來)