157 lines
4.2 KiB
Go
157 lines
4.2 KiB
Go
package top100liked
|
||
|
||
import (
|
||
"fmt"
|
||
"testing"
|
||
)
|
||
|
||
// https://leetcode.cn/problems/find-median-from-data-stream/?envType=study-plan-v2&envId=top-100-liked
|
||
//
|
||
// 思路:双堆法
|
||
// 维护两个堆,把数据流分成"较小的一半"和"较大的一半":
|
||
// - left:大顶堆,存较小的一半,堆顶是较小半部分的最大值
|
||
// - right:小顶堆,存较大的一半,堆顶是较大半部分的最小值
|
||
// 插入时根据当前中位数(left 堆顶)决定放入哪个堆,再平衡两堆大小,
|
||
// 保证 |left.Len() - right.Len()| <= 1,这样中位数只需看两堆顶即可。
|
||
|
||
// 泛型堆,通过 compareFunc 决定是大顶堆还是小顶堆。
|
||
// compareFunc(a, b) 返回 true 表示 a 应排在 b 上面(优先级更高)。
|
||
type heap[T any] struct {
|
||
data []T
|
||
compareFunc func(T, T) bool
|
||
}
|
||
|
||
func NewHeap[T any](compareFunc func(T, T) bool) heap[T] {
|
||
return heap[T]{
|
||
compareFunc: compareFunc,
|
||
}
|
||
}
|
||
|
||
// 上浮:从 idx 向上比较,不满足堆序就与父节点交换,直到满足或到根。
|
||
func (h *heap[T]) Up(idx int) {
|
||
for (idx-1)/2 >= 0 {
|
||
cur := h.data[idx]
|
||
p := h.data[(idx-1)/2]
|
||
if h.compareFunc(cur, p) {
|
||
h.data[idx], h.data[(idx-1)/2] = h.data[(idx-1)/2], h.data[idx]
|
||
idx = (idx - 1) / 2
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 下沉:从 idx 向下比较,与优先级更高的子节点交换,直到满足堆序或成为叶子。
|
||
func (h *heap[T]) Down(idx int) {
|
||
for {
|
||
l, r := 2*idx+1, 2*idx+2
|
||
m := idx
|
||
if l < len(h.data) && h.compareFunc(h.data[l], h.data[m]) {
|
||
m = l
|
||
}
|
||
if r < len(h.data) && h.compareFunc(h.data[r], h.data[m]) {
|
||
m = r
|
||
}
|
||
if m == idx {
|
||
break
|
||
}
|
||
h.data[idx], h.data[m] = h.data[m], h.data[idx]
|
||
idx = m
|
||
}
|
||
}
|
||
|
||
// 插入:追加到末尾后上浮,保持堆序。
|
||
func (h *heap[T]) Push(val T) {
|
||
h.data = append(h.data, val)
|
||
h.Up(len(h.data) - 1)
|
||
}
|
||
|
||
// 弹出堆顶:把末尾元素换到堆顶,删除末尾,再从堆顶下沉。
|
||
func (h *heap[T]) Pop() T {
|
||
if len(h.data) <= 0 {
|
||
var zero T
|
||
return zero
|
||
}
|
||
old := h.data[0]
|
||
h.data[0] = h.data[len(h.data)-1]
|
||
h.data = h.data[:len(h.data)-1]
|
||
h.Down(0)
|
||
return old
|
||
}
|
||
|
||
func (h *heap[T]) Top() T {
|
||
if len(h.data) > 0 {
|
||
return h.data[0]
|
||
}
|
||
var zero T
|
||
return zero
|
||
}
|
||
|
||
func (h *heap[T]) Len() int {
|
||
return len(h.data)
|
||
}
|
||
|
||
// MedianFinder 用双堆维护数据流的中位数。
|
||
type MedianFinder struct {
|
||
left heap[int] // 大顶堆,存较小的一半,堆顶是该半部分最大值
|
||
right heap[int] // 小顶堆,存较大的一半,堆顶是该半部分最小值
|
||
}
|
||
|
||
// Constructor:left 用 > 构成大顶堆,right 用 < 构成小顶堆。
|
||
func Constructor() MedianFinder {
|
||
return MedianFinder{
|
||
left: NewHeap(func(i1, i2 int) bool { return i1 > i2 }),
|
||
right: NewHeap(func(i1, i2 int) bool { return i1 < i2 }),
|
||
}
|
||
}
|
||
|
||
// AddNum 插入一个数并保持两堆平衡:
|
||
// 1. 第一个数直接放 left;
|
||
// 2. 后续数与 left 堆顶比较:>= 堆顶放 right(属于较大半部分),否则放 left;
|
||
// 3. 平衡:任一堆比另一堆多超过 1,就搬堆顶到另一侧。
|
||
func (this *MedianFinder) AddNum(num int) {
|
||
if this.left.Len() == 0 {
|
||
this.left.Push(num)
|
||
return
|
||
}
|
||
if num >= this.left.Top() {
|
||
this.right.Push(num)
|
||
} else {
|
||
this.left.Push(num)
|
||
}
|
||
if this.left.Len() > this.right.Len()+1 {
|
||
v := this.left.Pop()
|
||
this.right.Push(v)
|
||
}
|
||
if this.right.Len() > this.left.Len()+1 {
|
||
v := this.right.Pop()
|
||
this.left.Push(v)
|
||
}
|
||
}
|
||
|
||
// FindMedian 根据两堆大小关系求中位数:
|
||
// - 总数为偶数:取两堆顶的平均值
|
||
// - 总数为奇数:元素多的那堆堆顶就是中位数
|
||
func (this *MedianFinder) FindMedian() float64 {
|
||
switch (this.left.Len() + this.right.Len()) % 2 {
|
||
case 0:
|
||
return (float64(this.left.Top()) + float64(this.right.Top())) / 2
|
||
case 1:
|
||
if this.left.Len() > this.right.Len() {
|
||
return float64(this.left.Top())
|
||
} else {
|
||
return float64(this.right.Top())
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func Test(t *testing.T) {
|
||
medianFinder := Constructor()
|
||
medianFinder.AddNum(1) // arr = [1]
|
||
medianFinder.AddNum(2) // arr = [1, 2]
|
||
fmt.Println(medianFinder.FindMedian()) // 返回 1.5 ((1 + 2) / 2)
|
||
medianFinder.AddNum(3) // arr[1, 2, 3]
|
||
fmt.Println(medianFinder.FindMedian()) // return 2.0
|
||
}
|