Description
設計一個資料結構,支援從資料流中新增整數,並能隨時找到目前所有已加入整數的中位數。若元素個數為偶數,中位數為中間兩數的平均值。
Example:
Input: [“MedianFinder”, “addNum”, “addNum”, “findMedian”, “addNum”, “findMedian”] [[], [1], [2], [], [3], []] Output: [null, null, null, 1.5, null, 2.0]
Intuition#
核心思路:用兩個 Heap(Max Heap 存較小半邊,Min Heap 存較大半邊),中位數就在兩個堆頂之間。
- 中位數把資料分成「較小的一半」和「較大的一半」
- Max Heap(左半邊)的堆頂是較小一半的最大值
- Min Heap(右半邊)的堆頂是較大一半的最小值
- 維持兩個 Heap 大小差不超過 1,中位數就在堆頂
Approaches#
1: Sorted List(插入排序)#
- 概念: 維護一個有序列表,每次用二分搜尋找到插入位置,中位數直接取中間元素
- 時間複雜度:
O(n)addNum(插入需要移動元素),O(1)findMedian - 空間複雜度:
O(n)
class MedianFinder() {
private val sorted = mutableListOf<Int>()
fun addNum(num: Int) {
var pos = sorted.binarySearch(num)
if (pos < 0) pos = -(pos + 1)
sorted.add(pos, num)
}
fun findMedian(): Double {
val n = sorted.size
return if (n % 2 == 1) {
sorted[n / 2].toDouble()
} else {
(sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
}
}
}⭐2: Two Heaps#
- 概念: Max Heap 存較小半邊,Min Heap 存較大半邊。新數先加入 Max Heap,再平衡兩邊大小。中位數從堆頂取得
- 時間複雜度:
O(log n)addNum,O(1)findMedian - 空間複雜度:
O(n)
class MedianFinder() {
private val small = PriorityQueue<Int>(compareByDescending { it }) // Max Heap (左半邊)
private val large = PriorityQueue<Int>() // Min Heap (右半邊)
fun addNum(num: Int) {
small.offer(num)
// 確保 small 的最大值 <= large 的最小值
if (large.isNotEmpty() && small.peek() > large.peek()) {
large.offer(small.poll())
}
// 平衡大小:small 最多比 large 多 1 個
if (small.size > large.size + 1) {
large.offer(small.poll())
} else if (large.size > small.size) {
small.offer(large.poll())
}
}
fun findMedian(): Double {
return if (small.size > large.size) {
small.peek().toDouble()
} else {
(small.peek() + large.peek()) / 2.0
}
}
}平衡邏輯的順序很重要:先確保值的正確性(小堆的最大 <= 大堆的最小),再平衡大小。順序反過來可能導致錯誤。
🔑 Takeaways#
- Pattern: Two Heaps 是維護動態中位數的經典模式,也適用於滑動視窗中位數等變體
- 關鍵技巧: Max Heap + Min Heap 的組合,讓「較小半邊的最大值」和「較大半邊的最小值」都能 O(1) 取得