幾種常見的排序算法總結(JavaScript 描述)

本文討論:冒泡排序、插入排序、希爾排序、簡單搜索排序、快速排序、歸併排序、堆排序。

冒泡排序

介紹

冒泡排序比較任何兩個相鄰的項。如果前一個比後一個大,就交換它們。元素向上移動至正確的位置,看上去就像水中上升的氣泡一樣。

代碼

function bubbleSort(arr) {
    let length = arr.length;
    let flag;
    for (let i = length - 1; i >= 1; i--) { // 大等於 1 ,是因爲 j 從 1 開始,比較前一項和第 j 項
        flag = 0;

        for (let j = 1; j <= i; j++) { // 從第二項到第i項,保證它們比它們的前一項大。其中,第 i 項就是無序序列最後一項。本次內循環之後就成了有序序列的前一項。
            if (arr[j - 1] > arr[j]) {
               let t = arr[j - 1];
               arr[j - 1] = arr[j];
               arr[j] = t;
               flag = 1;
            }
        }
        if (flag === 0) return arr; // 當本次內循環,即本次遍歷無序序列的過程中,沒有發生冒泡,即所有元素都比其前一項大,說明序列本身已經變成有序序列
    }
}

注意點

  • 設定無序序列在左邊,有序序列在右邊。
  • 最開始,全是無序序列(0 到 length-1)。冒泡排序過程,就是右邊的有序序列從無到有一路左擴的過程。
  • 外層循環的i 標識有序序列的長度變化,即從最右 arr[arr.length - 1] 到最左arr[1] 的過程。
  • 內層循環的j是每次遍歷無序序列的指針,比較arr[j-1]是否比 arr[j]大。是則把更大的右移。
  • 引入變量 flag 減少無用的循環。然並卵,和其他比依然慢。

時間複雜度

  • O(n²)
  • 最好情況: 本身正序 O(n)
  • 最差情況: 本身反序 O(n²)

空間複雜度

冒泡排序算法的額外空間只有一個 t ,故而

  • O(1)

示意圖

冒泡排序


簡單選擇排序

介紹

簡單選擇排序是一種原址比較排序算法。
其思路是:找到數據結構中的最小值,並把它放到第一位。接着找到第二小的值,並放到第二位 … … 以此類推。

即,從無序序列中找出最小的,把它與當前無序序列中最左端的交換

代碼

function selectSort(arr) {
    let i, j, t, minIndex;
    let len = arr.length;
    for (i = 0; i < len; i++) {
        minIndex = i;
        for (j = i + 1; j < len; j++) {
            // 當發現 j 指向的比 minIndex 更小,minIndex 保存 這個 j 
            if (arr[minIndex] > arr[j]) {
                minIndex = j;
            }
        }
        // 用內層循環找到 無序序列中 最小的值後,把它放到 無序序列中的最左端。由此,無序序列長度減一,有序序列長度加一。
        t = arr[minIndex];
        arr[minIndex] = arr[i];
        arr[i] = t;

    }
    return arr;
}

注意點

  • 剛開始認爲全是無序,有序序列從左向右擴張。
  • 外層循環:遍歷無序序列中每一個,每次都假設當前第 1 個(即 i)就是 minIndex
  • 內層循環:從無序序列中找出最小的。第一個先是和第二個比,再是和第三個第四個…,故 j=i+1
  • 當發現 j 指向的比 minIndex 更小,minIndex 保存 這個 j 。目的是:用 minIndex 保存無序序列中最小值的下標。
  • 無序序列中找出最小的之後,就它與當前無序序列中最左端的交換。由此,無序序列長度減一,有序序列長度加一。

時間複雜度

  • O(n²)
  • 最好情況: O(n²)
  • 最差情況: O(n²)
  • 嗯 … 慢得很穩定

空間複雜度

簡單選擇排序算法的額外空間只有一個 t ,故而

  • O(1)

示意圖

簡單選擇排序


直接插入排序

介紹

假設第一項已經排序,從第二項開始,一一與有序序列進行比對,確定其插入位置。

代碼

function insertSort(arr) {
  let i, j, t;
  let len = arr.length;
  for (i = 1; i < len; i++) {
    t = arr[i];
    j = i - 1;
    // 插入排序,插的位置就是 j+1
    while (j < len && arr[j] > t) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = t;
  }
  return arr;
}

注意點

  • 最開始,默認第一個爲紅色有序,其餘爲藍色無序。插入排序的過程,就是右側藍色無序序列漸漸減減少,元素一個個插入左側紅色有序序列中的過程。
  • 外層 for 循環:遍歷無序序列,故從 arr[1]arr[len-1];每次先把該值保存到 t
  • 內層 while :把 arr[i] 與 左邊紅色有序序列 中的每一項進行對比,從右到左。 故 j = i-1
    *j 指針不斷前移,尋找更比 t 小的值。
  • 當未找到時,即前值 arr[j] 覆蓋後值 arr[j+1],使得空位前移
    • 空位是指該值已經賦給別的變量,此處可以被覆蓋的位置
  • 當找到,用 tarr[j+1] 且不再移動指針 j

時間複雜度

  • O(n²)
  • 最好情況: 初始序列有序 O(n)
  • 最差情況: 初始序列逆序 O(n²)

空間複雜度

直接插入排序算法的額外空間只有一個 t ,故而

  • O(1)

示意圖

插入排序


希爾排序

介紹

希爾排序又叫縮小增量排序,是優化後的插入排序。
插入排序最耗時的部分就是元素的移動。初始序列越有序,元素移動越少,耗時也就越少。
希爾排序引入了增量的規則,將待排序列分爲若干子序列,再分別對子序列進行直接插入排序。通過這種方法,顯著減少了元素移動,將算法的時間複雜度優化到 O(nlog2 n)。

代碼

function shellSort(arr) {
  let gap, i, j, t;
  let len = arr.length;
  for (gap = Math.floor(len / 2); gap >= 1; gap = Math.floor(gap / 2)) {
    for (i = gap; i < len; i++) {
      t = arr[i];
      j = i - gap;
      for (j = i; j > 0; j -= gap) {
        if (arr[j] > t) {
          arr[j] = arr[j - gap];
        }
      }
      arr[j] = t;
    }
  }
  return arr;
}

注意點

  • 和直接插入排序相比,多加的一層循環用來設置 gap
    • 初始值一般數組長度/2 向下(上)取整;每次減半;直到爲 1.
    • 這就是它叫“縮小增量排序”的原因。
  • 中層(原插排第一層循環):i = gap。從本輪的增量值開始,一一進行比對。
    • 直接插入排序相當於 gap = 1
  • 內層:作用同直接插入排序

時間複雜度

  • O(nlog2 n)

空間複雜度

同直接插入排序算法

  • O(1)

二路歸併排序

介紹

歸併排序是一種分治算法。
其思想:

  • 將 原始數組 切分成較小的數組,直到每個小數組只有 一個位置 ;
  • 接着將 小數組 歸併 成較大的數組,直到最後只有一個排序完畢的大數組。

代碼

// 歸併排序: 先切分 再合併
function mergeSort(arr) { 
    let len = arr.length;
    let mid = Math.floor(len / 2); // 找中間位置

    if (len < 2) { // 終止遞歸的條件
        return arr;
    }
    let left = arr.slice(0, mid); // 切分數組爲左右兩段
    let right = arr.slice(mid);

    left = mergeSort(left); // 對左右兩段分別遞歸執行歸併排序
    right = mergeSort(right);

    return merge(left, right); // 最後 合併小數組直到成爲一個大數組
}

// merge 函數作用:對小數組進行 合併 與 排序,來產生大數組,直到回到原始數組並已排序完成
function merge(left, right) {
    let result = [];

    while (left.length > 0 && right.length > 0) {
        // 當 左右數組 都還有,就比較二者,將較小的推入結果數組
        // 如果推入較大的,就是從大到小的排序
        result.push(left[0] < right[0] ? left.shift() : right.shift()); // 之所以比較各自第 0 項,是因爲 shift ; shift 等方法的參數與返回值
    }
    // 二者比較完,把剩下的數組推入結果數組
    while (left.length) result.push(left.shift());
    while (right.length) result.push(right.shift());

    return result;
}

時間複雜度

  • O(nlog2 n)
  • 最快 O(nlog2 n)
  • 最慢 O(nlog2 n)

空間複雜度

歸併排序需要轉存整個待排序列,故而

  • O(n)

示意圖

歸併排序


快速排序

介紹

快速排序可能是最常用的排序算法。
同歸並排序一樣,快速排序也使用了分治的方法,將原始數組分爲較小的數組。
就平均時間而言,快速排序是所有排序算法中性能最好的。

代碼

function quickSort(arr, low, high) {
    let i = low,
    j = high;
    let t;
    if (low < high) {// 只有當 low 在 high 左的時候才執行。遞歸執行時,當 low = high 就不執行了。             
        t = arr[i]; // 將第一個作爲基準,t 保存該基準值。下面開始 i, j 指針的操作。
        // 僅在 i < j 時進行如下指針操作
        while (i < j) {
        // 右指針左移,直到找到比 t 小的值
            while (i < j && arr[j] >= t) j--;
            // 當右指針找到右側比基準值小的值時,就把它賦值給左指針,左指針右/後移
            if (i < j) {
                arr[i] = arr[j];
                i++;
            }
            // 對稱地,左左指針右移,直到找到比 t 大的值
            while (i < j && arr[i] <= t) i++;
                    // 當在基準值左側找到比 t 大的值,用它覆蓋掉右指針指向的值,並讓右指針前移
            if (i < j) {
                arr[j] = arr[i];
                j--;
            }    
        }
        // 當 i, j 指針相遇,則將其指向的值作爲新的基準值? 將之前 t 保存的基準值賦值給 arr[i]
        arr[i] = t;
        // 對基準值(第 i 項)前,後兩段數組分別遞歸執行快排
        quickSort(arr, low, i - 1);
        quickSort(arr, i + 1, high);
    }
    return arr;
}

注意點

  • 參數 low high
  • [if] 只有當 low < high 才執行,否則返回。算是結束遞歸執行的條件
    • 當執行,先選定一個基準值 保存到 t
    • [while] 僅在 i < j 時進行指針相向運動操作
      • [while] 右指針 j 左移,直到找到比基準值 t 更小的 [if] 就把它賦值給左指針 i 指向的值,並右移左指針
      • [while] 左指針 i 右移,直到找到比基準值 t 更大的 [if] 就把它賦值給右指針 j 指向的值,並左移右指針
    • 當 i, j 指針相遇,即找到了基準位置,用 t 賦值;並對左右兩端進行遞歸操作

時間複雜度

待排序列越接近無序,快排算法效率越高;
待排序列越接近有序,快排算法效率越低。

  • O(nlog2 n)
  • 最好情況 O(nlog2 n)
  • 最壞情況 O(n²)

空間複雜度

快速排序是遞歸進行的,遞歸需要棧的輔助,因此它需要的輔助空間較多。

  • O(log2 n)

示意圖

快速排序


堆排序

介紹

堆是一種數據結構,可以把堆看成一顆完全二叉樹。
這顆完全二叉樹滿足:任何一個 非葉節點 的值都不小於(大於)其左右孩子節點的值。
若父親大孩子小,稱爲大頂堆;反之則稱爲小頂堆

堆排序的思想:
根據堆的定義,代表這顆完全二叉樹的根節點的值是最大(小)的,因此將一個無序序列調整爲一個堆,就可以找出這個序列的最大(小)值,然後將找出的這個值交換到序列的最後(前)。
這樣,有序序列元素增加一個,無序序列元素減少一個。對新的無序序列重複操作,就實現了排序。

堆排序中最關鍵的操作是將序列調整爲堆。整個排序過程就是通過不斷調整使得不符合堆定義的完全二叉樹變爲符合堆定義的完全二叉樹的過程。

先建堆,再排序。

這是可以使用的規律:
堆

代碼

let len;    // 全局變量

function buildMaxHeap(arr) {   // 建立大頂堆
    len = arr.length;
    for (let i = Math.floor(len/2); i >= 0; i--) {
        heapify(arr, i);
    }
}

function heapify(arr, i) {     // 堆調整
    let left = 2 * i + 1,
        right = 2 * i + 2,
        largest = i;

    if (left < len && arr[left] > arr[largest]) {
        largest = left;
    }

    if (right < len && arr[right] > arr[largest]) {
        largest = right;
    }

    if (largest != i) {
        swap(arr, i, largest);
        heapify(arr, largest);
    }
}

function swap(arr, i, j) {
    let temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

function heapSort(arr) {
    buildMaxHeap(arr);

    for (let i = arr.length-1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        heapify(arr, 0);
    }
    return arr;
}

時間複雜度

  • O(nlog2 n)

注:
部分圖片引用自 github.com/Wscats/CV/issues/14

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章