Partition 搜尋第 k 個數字

Partition 搜尋第 k 個數字

找出第 k 小的數字,使用 partition 將陣列切割

QuickSelect 的 Partition 切割

一般的排序需要將所有的數字都排序過,所以必須比對所有的數字,所以時間複雜度至少需要 nlog(n),當排序的輸字陣列越多,時間複雜度就會越多

所以如果要能夠在數字越多的狀況下,也能夠有平均速度以上的排序速度,可以選擇 QuickSelect 排序選擇方式,時間複雜度最快可以是 O(n),但最慢可能會到 O(n^2)

在數字陣列比較小的狀況,直接排序 是比較快的,但在數字陣列可大可小的狀況下,QuickSelect 平均速度是都能夠在水準以上的

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

搜尋第 K 大的數字

索引是從 0 開始計算,第 k 大等於是索引第 length - k 個索引數字

TypeScript

/**
 * @param {number[]} numsList
 * @param {number} kth
 * @return {number}
 */
var findKthLargest = function(numsList, kth) {
    // 選擇第 k 大的數字 : 在排序的數字陣列中,選擇 (length - k) 陣列索引的數字
    let kIndexNumber= numsList.length - kth;
    // 左指標:從陣列頭開始
    let leftPointer = 0;
    // 右指標:從陣列尾開始
    let rightPointer = numsList.length - 1;
    while (leftPointer < rightPointer) {
        // 當左右指標沒有交叉,繼續尋找
        let partitionPointer = partitionNumberList(numsList, leftPointer, rightPointer);

        if (partitionPointer == kIndexNumber) {
            // 目前切割的點,就是要尋找的第 k 個索引的數字
            break;
        } else if (partitionPointer < kIndexNumber) {
            // 目前切割的點,比要找的 k 個索引數字還小,切割點左邊確認數字更小不需要找,往切割點右邊部分繼續找
            // 將「左指標」移動到「切割點」右方
            leftPointer = partitionPointer + 1;
        } else {
            // 目前切割的點,比要找的 k 個索引數字還大,切割點右邊確認數字更大不需要找,往切割點左邊部分繼續找
            // 將「右指標」移動到「切割點」左方
            rightPointer = partitionPointer - 1;
        }
    }

    return numsList[kIndexNumber];
};

// 切割數字陣列
var partitionNumberList = function(numsList, leftPointer, rightPointer) {
    // 選擇右指標當作「檢查數字」
    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;
};

搜尋第 K 小的數字

索引是從 0 開始計算,第 k 小等於是索引第 k 個索引數字

TypeScript

function findKthSmallestNumber(numsList: number[], kth: number): any {
    // 設定指標
    let leftPointer = 0;
    let rightPointer = numsList.length - 1;

    while (leftPointer < rightPointer) {
        // 切割點
        let partitionPointer = partitionNumberList(numsList, leftPointer, rightPointer);

        if (partitionPointer == kth) {
            // 如果「切割點」剛好是第 k 個數字,不需要再找了,直接回傳切割點指標數字
            break;
        }

        if (partitionPointer < kth) {
            // 如果「切割點」比第 k 個數字還要小,表示目前切割點是偏左,在 k 的左邊
            // 所以左邊的數字一定是比較小不用找,可能的答案在右邊
            // 往右方部分找,找下一個切割點
            leftPointer = partitionPointer + 1;
        } else {
            // 如果「切割點」比第 k 個數字還要大,表示目前切割點是偏右,在 k 的右邊
            // 所以右邊的數字一定是比較大不用找,可能的答案在左邊
            // 往左方部分找,找下一個切割點
            rightPointer = partitionPointer - 1;
        }
    }

    return numsList[kth];
}


// 切割數字陣列
var partitionNumberList = function(numsList: number[], leftPointer: number, rightPointer: number): 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;
};