215. Kth Largest Element in an Array

Leetcode 問題: 215. Kth Largest Element in an Array

題目

Given an integer array nums and an integer k, return the kth largest element in the array.

Note that it is the kth largest element in the sorted order, not the kth distinct element.

傳入數字陣列 nums 及排名 k,找到陣列中第 k 大的數字

Example 1

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

Example 2

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

答案

JavaScript

1. 數字陣列先排序

/**
 * @param {number[]} numsList
 * @param {number} kth
 * @return {number}
 */
var findKthLargest = function(numsList, kth) {
    // 排序數字列(小到大排序)
    numsList.sort(function(a, b) {
        return a - b;
    });

    // 越後面數字越大,從後面找第 k 排名的數字
    let kthNumber = numsList[numsList.length - kth];

    return kthNumber;
};

2. 使用 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
/**
 * @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;
};

參考資料