🎯 什麼情境該想到我
當你「只要第 個,不需要全部排好」的時候——中位數、p95/p99 延遲、Top-K 熱門、離群值門檻。
★ 完整排序是 ,但選擇只需要 。 而且實務上有三種做法,選錯差很多。
⚙️ 怎麼用(三種做法與選擇判準)
★ 三種做法的對照(這張表是本頁的核心)
| 做法 | 時間 | 空間 | 何時用 |
|---|---|---|---|
| ① 先排序再取 | 或 | 接近 ;或本來就要排序;或 很小(別過度優化) | |
| ② 大小為 的堆 | ★ ,或資料是串流/裝不進記憶體 | ||
| ③ Quickselect | 平均 ,最差 | ★ 資料全在記憶體、只要一次、 任意 |
★ 實務上的判準:
- 資料是串流/太大裝不下 → ② 堆(這是唯一可行的)
- 要算 p99 這種 很大的 → ② 的堆會變成 ,改用 ③ 或 ①
- 全在記憶體、一次性 → ③ Quickselect
- 會重複查詢不同的 → ① 排序一次,之後每次
② 堆的做法(Top-K 最常用)
要「最大的 個」→ 維護一個大小為 的「最小堆」(反直覺但正確):
import "container/heap"
// 最大的 k 個:用最小堆,堆頂是「目前 top-k 裡最小的」
func TopK(stream []int, k int) []int {
h := &IntHeap{} // 最小堆
heap.Init(h)
for _, v := range stream {
if h.Len() < k {
heap.Push(h, v)
} else if v > (*h)[0] { // ★ 比目前最小的還大,才值得換進來
heap.Pop(h)
heap.Push(h, v)
}
}
return *h
}★ 為什麼是最小堆:堆頂是這 個裡最小的,也就是「門檻」。
新元素只要不超過門檻就直接丟掉—— 判斷。超過才付 把門檻換掉。
★ 它的關鍵優勢:只需要 記憶體,而且是串流演算法(資料只掃一遍、不用全部留著)。
Go 的 container/heap 官方定義就是最小堆(“each node is the minimum-valued node in its subtree”,見 二元堆積)。
③ Quickselect(平均線性)
就是 快速排序,但分割後只遞迴「包含答案的那一邊」。
// 回傳第 k 小的元素(k 從 0 開始),會原地打亂 a
func QuickSelect[T cmp.Ordered](a []T, k int) T {
for {
if len(a) == 1 {
return a[0]
}
p := partition(a) // 見 [[快速排序]]
switch {
case k == p:
return a[p]
case k < p:
a = a[:p] // ★ 只遞迴左邊
default:
a = a[p+1:] // ★ 只遞迴右邊,k 要跟著平移
k -= p + 1
}
}
}★ 為什麼平均是 而不是 :
快排每層要處理所有元素(兩邊都遞迴),所以是 ( 層)。
Quickselect 每層只處理一半:
等比級數收斂成常數倍 —— 這就是「只遞迴一邊」帶來的量級差異。
同樣的道理也解釋了 二分搜尋 為什麼是 而不是 。
最差仍是 (每次都切出極端不平衡),所以要隨機化 pivot。
Median of Medians(最差 )
CLRS 的經典結果:存在最差情況也是 的選擇演算法。
做法是把元素每 5 個一組取中位數,再遞迴求「這些中位數的中位數」當 pivot,
可證明這個 pivot 至少能排除 30% 的元素。
⚠️ 理論上漂亮,實務上幾乎沒人用——常數太大,隨機化的 quickselect 在真實資料上快得多。
它的價值是「證明線性選擇是可能的」,不是拿來寫。
★ 實務:p95 / p99 延遲怎麼算
直接排序全部樣本是最簡單也最準的——如果樣本放得下。
大規模時的實務做法:
| 方法 | 說明 |
|---|---|
| 固定寬度直方圖/bucket | 預先定好 bucket 邊界(如 1ms, 2ms, 5ms, 10ms…),只累加計數。記憶體固定、可合併,代價是解析度受 bucket 粒度限制 —— Prometheus histogram 就是這樣 |
| t-digest / HDR Histogram | 專門為分位數設計,尾端解析度高(p99、p999 準),可合併 |
| ★ 可合併性是關鍵 | 多台機器的分位數不能直接平均!「三台機器的 p99 各是 100ms」不代表整體 p99 是 100ms。 必須用可合併的結構(直方圖、t-digest)在原始資料層合併後再算 |
★ 「p99 不能平均」是實務上極常見的錯誤,值得單獨記住。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ Quickselect 會修改輸入陣列(原地分割)。不能動原始資料就要先複製,那就多了 空間。
- Quickselect 必須隨機化 pivot,否則已排序輸入會退化成 (同 快速排序)。
- ★ 資料是串流時只能用堆。Quickselect 需要全部資料在手上。
- 很大時堆的做法會退化。(中位數)時 ,還不如直接排序。
- 重複值很多時,quickselect 要用三路分割,否則會退化。
- 多台機器的分位數不可平均(見上)。
- 很小就直接排序。 時所有方法都是微秒級,選最好懂的那個。