Partition 搜尋第 k 個數字
Categories:
找出第 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;
};