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;
};