Quick Sort 快速排序法

Quick Sort 快速排序法

複雜度

複雜度 Big O
最差時間複雜度 Worst-case time complexity Big O(n log n)
平均時間複雜度 Average-case time complexity Big theta (n log n)
最佳時間複雜度 Best-case time complexity Big omega (n log n)
空間複雜度 Space complexity Big O (n)

演算法

1. 取得數字陣列 pivot 分割中心點

確保 pivot 分割中心點 的左側數字小於 pivot 分割中心點 的數字

確保 pivot 分割中心點 的右側數字大於 pivot 分割中心點 的數字

2. 排序左右兩側數字陣列

pivot 分割中心點的數字及位置已確定

持續計算 pivot 分割中心點左側及右側陣列的子陣列

直到左側與右側已經遞迴比較到最後無法再切割

Quick Sort 快速排序法

Quick Sort 快速排序法

QuickSelect 流程

Input: nums = [3,2,1,5,6,4], k = 2
Output: 5

A. 選擇一用來比較的 pivotCheckNumber 當作檢查數字

  • 確保「檢查數字」左邊的數字,都是比「檢查數字」小
  • 確保「檢查數字」右邊的數字,都是比「檢查數字」大

一開始隨機選擇 最右邊 的數字當作檢查數字

索引 0 1 2 3 4 5
數字 3 2 1 5 6 4
檢查指標 左指標 右指標
其他指標 最小數字指標 檢查數字指標

目標要將比「檢查數字」 小的數字放在左邊,比較大的就放在「檢查數字」右邊

所以就拿「最小數字指標」當作儲存較小數字的指標位置,預設是最左邊

B. 從左指標檢查到右指標

「左指標」數字「檢查數字」 小,就將「左指標」數字 放到 「最小數字指標」,而 「最小數字指標」 就往右移動,等待放入下一個比 「檢查數字」 小的數字,然後持續檢查到右指標

Step 1

  • 左指標數字 3檢查數字指標數字 4 小,將 左指標數字 3最小數字指標 數值交換
  • 「最小數字指標」 往右移動
  • 左指標 往右移動
索引 0 1 2 3 4 5
數字 3 2 1 5 6 4
檢查指標 左指標 右指標
其他指標 最小數字指標 檢查數字指標

Step 2

  • 左指標數字 2檢查數字指標數字 4 小,將 左指標數字 2最小數字指標 數值交換
  • 「最小數字指標」 往右移動
  • 左指標 往右移動
索引 0 1 2 3 4 5
數字 3 2 1 5 6 4
檢查指標 左指標 右指標
其他指標 最小數字指標 檢查數字指標

Step 3

  • 左指標數字 1檢查數字指標數字 4 小,將 左指標數字 1最小數字指標 數值交換
  • 「最小數字指標」 往右移動
  • 左指標 往右移動
索引 0 1 2 3 4 5
數字 3 2 1 5 6 4
檢查指標 左指標 右指標
其他指標 最小數字指標 檢查數字指標

Step 4

  • 左指標數字 5檢查數字指標數字 4 大,不做任何動作
  • 左指標 往右移動
索引 0 1 2 3 4 5
數字 3 2 1 5 6 4
檢查指標 左指標 右指標
其他指標 最小數字指標 檢查數字指標

Step 5

  • 左指標數字 6檢查數字指標數字 4 大,不做任何動作
  • 左指標 往右移動
索引 0 1 2 3 4 5
數字 3 2 1 5 6 4
檢查指標 左指標 & 右指標
其他指標 最小數字指標 檢查數字指標

Step 6

  • 左指標 已經檢查到 右指標 位置了,不再繼續檢查
  • 檢查數字指標數字 4最小數字指標數字 5 交換
  • 所以目前切割的位置在 最小數字指標索引 3 的位置,數值是 4
    • 最小數字指標索引 3 的位置右邊都是比 4 大的數字
    • 最小數字指標索引 3 的位置左邊都是比 4 小的數字
  • 所以目前 最小數字指標索引 的切割位置及數字都是確定的
索引 0 1 2 3 4 5
數字 3 2 1 4 6 5
檢查指標 左指標 & 右指標
其他指標 最小數字指標 檢查數字指標

Step 7

因為要選擇第 k 大的數字,所以在排序的數字陣列中,選擇 (length - k) 陣列索引 的數字

  • 如果要選第 1 大的數字,等於是要選擇索引陣列中 (6 - 1) = 5 第 5 個索引 位置的數字
  • 如果要選第 2 大的數字,等於是要選擇索引陣列中 (6 - 2) = 4 第 4 個索引 位置的數字
  • 如果要選第 3 大的數字,等於是要選擇索引陣列中 (6 - 3) = 3 第 3 個索引 位置的數字
  • 如果要選第 4 大的數字,等於是要選擇索引陣列中 (6 - 4) = 2 第 2 個索引 位置的數字
  • …以此類推

狀況 1: 要找的數字是剛好是切割位置

確定的 切割位置最小數字指標,所以索引位置確定會是 3,數字確定會是 4

若剛好要找 第 3 大的數字,等於是要選擇索引陣列中 (6 - 3) = 3 第 3 個索引 位置的數字

等於 目前的切割點 就是要找的 第 k 大數字,可以直接回傳 4

索引 0 1 2 3 4 5
數字 3 2 1 4 6 5
檢查指標 左指標 & 右指標
其他指標 最小數字指標 檢查數字指標

狀況 2: 要找的數字是大小是大於切割位置的數字

假如要找 第 2 大 的數字,等於是要選擇索引陣列中 (6 - 2) = 4 第 4 個索引 位置的數字

但目前的切割位置是在 最小數字指標,第 3 個索引,而剛剛切割流程中,已經確定

  • 最小數字指標索引 3 的位置右邊都是比 4 大的數字
  • 最小數字指標索引 3 的位置左邊都是比 4 小的數字

表示現在要往 切割位置右方 去找,才能找到 第 2 大 的數字,所以就重複剛剛的步驟,重新對右半邊的數字陣列做切割比較即可

  • 可以將 左指標 設定在 切割位置 右邊 4
  • 右邊區塊的最小數字指標設定與 左指標 相同
  • 然後重複剛剛的步驟,直到找到 切割位置索引位置第 k 大數字 相同,符合 狀況 1: 要找的數字是剛好是切割位置 即可
索引 0 1 2 3 4 5
數字 3 2 1 4 6 5
檢查指標 左指標 右指標
其他指標 X X X X 最小數字指標 檢查數字指標

狀況 3: 要找的數字是大小是小於切割位置的數字

假如要找 第 4 大 的數字,等於是要選擇索引陣列中 (6 - 4) = 2 第 2 個索引 位置的數字

狀況 2 雷同,只是要反過來往 切割位置左方 去找,才能找到 第 4 大 的數字,所以就重複剛剛的步驟,重新對左半邊的數字陣列做切割比較即可

  • 可以將 右指標 設定在 切割位置 左邊 2
  • 左邊區塊的最小數字指標設定與 左指標 相同
  • 然後重複剛剛的步驟,直到找到 切割位置索引位置第 k 大數字 相同,符合 狀況 1: 要找的數字是剛好是切割位置 即可
索引 0 1 2 3 4 5
數字 3 2 1 4 6 5
檢查指標 左指標 右指標
其他指標 最小數字指標 檢查數字指標 X X X

程式: 使用左側第一個數字當作 pivot 數字

JavaScript

function partition(numberList, leftPointer, rightPointer) {
    var _a, _b;
    if (leftPointer === void 0) { leftPointer = 0; }
    if (rightPointer === void 0) { rightPointer = numberList.length - 1; }
    // 選擇第一個元素是 pivot 數字
    var pivotNumber = numberList[leftPointer];
    // 交換資料索引起始位置
    var swapPointer = leftPointer;
    // 與 pivot 數字比較,將較小的數字放左側,較大的數字放右側
    for (var i = leftPointer + 1; i <= rightPointer; i++) {
        // 從第 2 個數字開始檢查
        var currentCompareNumber = numberList[i];
        if (currentCompareNumber < pivotNumber) {
            // 「目前的數字」比「pivot 數字」還要小,要放在「pivot 數字」左方
            // 將「目前的數字」移動到「交換索引」位置
            swapPointer++;
            _a = [numberList[swapPointer], numberList[i]], numberList[i] = _a[0], numberList[swapPointer] = _a[1];
        }
    }
    // 將第 1 個 pivot 數字放到中間,這樣可以讓左側數字是小於 pivot 數字,右側數字大於 pivot 數字
    _b = [numberList[leftPointer], numberList[swapPointer]], numberList[swapPointer] = _b[0], numberList[leftPointer] = _b[1];
    // 回傳目前 Pivot 的中心點在哪
    return swapPointer;
}
function quickSort(sortNumberList, leftPointer, rightPointer) {
    if (leftPointer === void 0) { leftPointer = 0; }
    if (rightPointer === void 0) { rightPointer = sortNumberList.length - 1; }
    // Base case is that the left and right pointers don't overlap, after which we'll be left with an array of 1 item
    if (leftPointer < rightPointer) {
        // 如果「左指標」小於「右指標」,表示還沒比較完,可以繼續比較
        var pivotIndex = partition(sortNumberList, leftPointer, rightPointer);
        // 對中心點左側重新做 quick sort 排序,直到左側所有元素都按照順序排序
        quickSort(sortNumberList, leftPointer, pivotIndex - 1);
        // 對中心點右側重新做 quick sort 排序,直到右側所有元素都按照順序排序
        quickSort(sortNumberList, pivotIndex + 1, rightPointer);
    }
    return sortNumberList;
}
var items = [5, 3, 7, 6, 2, 9, -1, 11];
console.log(quickSort(items, 0, items.length - 1));

TypeScript

function partition(numberList: number[], leftPointer: number = 0, rightPointer: number = numberList.length - 1) {
    // 選擇第一個元素是 pivot 數字
    let pivotNumber = numberList[leftPointer];
    // 交換資料索引起始位置
    let swapPointer = leftPointer;

    // 與 pivot 數字比較,將較小的數字放左側,較大的數字放右側
    for (let i = leftPointer + 1; i <= rightPointer; i++) {
        // 從第 2 個數字開始檢查
        let currentCompareNumber = numberList[i];
        if (currentCompareNumber < pivotNumber) {
            // 「目前的數字」比「pivot 數字」還要小,要放在「pivot 數字」左方
            // 將「目前的數字」移動到「交換索引」位置
            swapPointer++;
            [numberList[i], numberList[swapPointer]] = [numberList[swapPointer], numberList[i]];
        }
    }

    // 將第 1 個 pivot 數字放到中間,這樣可以讓左側數字是小於 pivot 數字,右側數字大於 pivot 數字
    [numberList[swapPointer], numberList[leftPointer]] = [numberList[leftPointer], numberList[swapPointer]];

    // 回傳目前 Pivot 的中心點在哪
    return swapPointer;
}

function quickSort(sortNumberList: number[], leftPointer: number = 0, rightPointer: number = sortNumberList.length - 1) {
    // Base case is that the left and right pointers don't overlap, after which we'll be left with an array of 1 item
    if (leftPointer < rightPointer) {
        // 如果「左指標」小於「右指標」,表示還沒比較完,可以繼續比較
        let pivotIndex = partition(sortNumberList, leftPointer, rightPointer);

        // 對中心點左側重新做 quick sort 排序,直到左側所有元素都按照順序排序
        quickSort(sortNumberList, leftPointer, pivotIndex - 1);

        // 對中心點右側重新做 quick sort 排序,直到右側所有元素都按照順序排序
        quickSort(sortNumberList, pivotIndex + 1, rightPointer);
    }

    return sortNumberList;
}

let items = [5, 3, 7, 6, 2, 9, -1, 11];

console.log(quickSort(items, 0, items.length - 1));

程式: 使用右側第一個數字當作 pivot 數字

TypeScript

// 切割數字陣列
var partitionNumberList = function(numsList:number[], leftPointer: number, rightPointer:number) {
    // 選擇右指標當作「檢查數字」
    let pivotCheckNumber = numsList[rightPointer];
    // 較小數字紀錄指標設定為左指標
    let smallerNumberPointer = leftPointer;

    for(let i = leftPointer; i < rightPointer; i++) {
        // 從「左指標」檢查到「右指標」
        if (numsList[i] <= pivotCheckNumber) {
            // 若「目前檢查的數字」小於等於「檢查的數字」
            // 將目前數字放到「較小數字記錄指標」位置
            [numsList[smallerNumberPointer], numsList[i]] = [numsList[i], numsList[smallerNumberPointer]];
            // 「較小數字記錄指標」位置往後移動
            smallerNumberPointer++;
        }
    }
    // 檢查完成,確認「較小數字記錄指標」數字都比「檢查數字」小
    // 「較小數字記錄指標」與「檢查數字」交換
    //  - 「較小數字記錄指標」左方都是比「檢查數字」小的數字
    //  - 「較小數字記錄指標」右方都是比「檢查數字」大的數字
    [numsList[smallerNumberPointer], numsList[rightPointer]] = [numsList[rightPointer], numsList[smallerNumberPointer]];

    return smallerNumberPointer;
};


function quickSort(sortNumberList: number[], leftPointer: number = 0, rightPointer: number = sortNumberList.length - 1) {
    // Base case is that the left and right pointers don't overlap, after which we'll be left with an array of 1 item
    if (leftPointer < rightPointer) {
        // 如果「左指標」小於「右指標」,表示還沒比較完,可以繼續比較
        // 取得數字陣列 pivot 分割中心點
        let pivotIndex = partitionNumberList(sortNumberList, leftPointer, rightPointer);

        // 對中心點左側重新做 quick sort 排序,直到左側所有元素都按照順序排序
        quickSort(sortNumberList, leftPointer, pivotIndex - 1);

        // 對中心點右側重新做 quick sort 排序,直到右側所有元素都按照順序排序
        quickSort(sortNumberList, pivotIndex + 1, rightPointer);
    }

    return sortNumberList;
}

let items = [5, 3, 7, 6, 2, 9, -1, 11];

console.log(quickSort(items, 0, items.length - 1));

參考資料