This commit is contained in:
2026-07-23 16:56:50 +08:00
parent 124ad9afdf
commit 92784ffb0f
36 changed files with 2015 additions and 608 deletions
+193 -95
View File
@@ -1,134 +1,232 @@
package top100liked
import (
"fmt"
"container/heap"
"reflect"
"sort"
"testing"
)
// https://leetcode.cn/problems/find-median-from-data-stream/?envType=study-plan-v2&envId=top-100-liked
// https://leetcode.cn/problems/top-k-frequent-elements/?envType=study-plan-v2&envId=top-100-liked
// 347. 前 K 个高频元素
//
// 给一个整数数组 nums 和整数 k,返回出现频率前 k 高的元素,顺序任意。
// 进阶要求时间复杂度优于 O(n log n)。
//
// 通用第一步:统计频率 map[int]int。下面三种解法都基于此。
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,
// countFreq 统计每个元素出现次数。
func countFreq(nums []int) map[int]int {
m := make(map[int]int)
for _, v := range nums {
m[v]++
}
return m
}
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
// 解法一:最小堆 — O(n log k) / O(n)
// 维护大小为 k 的最小堆,堆顶是当前堆内频率最小者。
// 逐个把 (元素, 频率) 入堆;堆大小超过 k 时弹出堆顶,淘汰频率最小的。
// 遍历结束,堆里剩下的 k 个即前 k 高频元素。
//
// 实现要点(container/heap):
// - 定义 type entry struct{ val, cnt int}
// - 定义 type minHeap []entry,实现 heap.Interface
// - Less 按 cnt 升序(堆顶最小)
// - 遍历 mheap.Push(h, e)len(*h) > k 时 heap.Pop(h)
// - 收集堆中剩余元素返回
type entry struct {
val int
cnt int
}
type minHeap struct {
data []entry
}
// 上浮:新元素放末尾后,与父比较,比父小则交换向上
// i 是需要 siftUp 的元素的 index
func (h *minHeap) siftUp(i int) {
for i > 0 {
p := (i - 1) / 2
if h.data[p].cnt > h.data[i].cnt {
h.data[p], h.data[i] = h.data[i], h.data[p]
i = p
} else {
break
}
}
}
func (h *heap[T]) Down(idx int) {
// 下沉:从 i 开始,与较小子比较,比子大则交换向下。n 是有效堆大小
func (h *minHeap) siftDown(i, n 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
l, r := 2*i+1, 2*i+2
smallest := i
if l < n && h.data[l].cnt < h.data[smallest].cnt {
smallest = l
}
if r < len(h.data) && h.compareFunc(h.data[r], h.data[m]) {
m = r
if r < n && h.data[r].cnt < h.data[smallest].cnt {
smallest = r
}
if m == idx {
if smallest == i {
break
}
h.data[idx], h.data[m] = h.data[m], h.data[idx]
idx = m
h.data[smallest], h.data[i] = h.data[i], h.data[smallest]
i = smallest
}
}
func (h *heap[T]) Push(val T) {
h.data = append(h.data, val)
h.Up(len(h.data) - 1)
// push:追加末尾 + 上浮
func (h *minHeap) push(e entry) {
idx := len(h.data)
h.data = append(h.data, e)
h.siftUp(idx)
}
func (h *heap[T]) Pop() T {
if len(h.data) <= 0 {
var zero T
return zero
// popMin 弹出堆顶(最小 cnt):用末尾覆盖堆顶,缩容,下沉
func (h *minHeap) popMin() entry {
if len(h.data) == 0 {
return entry{}
}
old := h.data[0]
ret := 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]
h.siftDown(0, len(h.data))
}
var zero T
return zero
return ret
}
func (h *heap[T]) Len() int {
return len(h.data)
}
func (h *minHeap) len() int { return len(h.data) }
type MedianFinder struct {
left heap[int] // 小
right heap[int] // 大
}
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 }),
}
}
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)
}
}
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())
func topKFrequentHeap(nums []int, k int) []int {
m := countFreq(nums)
h := minHeap{}
for v, c := range m {
h.push(entry{v, c})
if h.len() > k {
h.popMin()
}
}
return 0
res := make([]int, 0, k)
for h.len() > 0 {
res = append(res, h.popMin().val)
}
return res
}
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
// 解法二:桶排序 — O(n) 平均 / O(n)
// 频率取值范围 [0, n],开长度 n+1 的桶:buckets[f] 存所有频率为 f 的元素。
// 从 buckets[n] 倒序向 buckets[1] 遍历,依次把桶内元素加入结果,凑够 k 个停止。
//
// 实现要点:
// - buckets := make([][]int, len(nums)+1)
// - for val, cnt := range m { buckets[cnt] = append(buckets[cnt], val) }
// - for f := len(buckets)-1; f >= 0 && len(res) < k; f-- { res = append(res, buckets[f]...) }
func topKFrequentBucket(nums []int, k int) []int {
m := countFreq(nums)
buckets := make([][]int, len(nums)+1)
for v, c := range m {
buckets[c] = append(buckets[c], v)
}
res := make([]int, 0, k)
for f := len(buckets) - 1; f >= 0 && len(res) < k; f-- {
res = append(res, buckets[f]...)
}
return res
}
// 解法三:container/heap 维护大小 k 的最小堆 — O(n log k) / O(n)
// 思路同解法一,但用标准库 container/heap 提供堆算法,自己只实现 heap.Interface。
//
// container/heap 两层 API(最容易踩坑的地方):
// - 你实现的 Push/Pop 只是"回调"Push 追加到切片末尾,Pop 删除末尾元素
// - 真正对外的是 heap.Push / heap.Pop:它们调你的 Push/Pop 并负责 siftUp/siftDown
// - Less 用 < 是小顶堆(堆顶最小),用 > 是大顶堆
// - Push/Pop 必须用指针接收者(要改 slice header);Len/Less/Swap 用值接收者即可
//
// 要点:
// - entryHeap []entry 实现 heap.InterfaceLen/Less/Swap + Push/Pop
// - Less 按 cnt 升序,堆顶是堆内频率最小者
// - 遍历 mheap.Push(h, entry{v,c})h.Len() > k 时 heap.Pop(h) 淘汰最小
// - 收集堆中剩余 k 个元素返回
type entryHeap []entry
func (h entryHeap) Len() int { return len(h) }
func (h entryHeap) Less(i, j int) bool { return h[i].cnt < h[j].cnt } // < 小顶堆
func (h entryHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
// Push/Pop 是给 heap 包调用的回调,只负责在切片末尾增删,不要加堆序逻辑
func (h *entryHeap) Push(x any) { *h = append(*h, x.(entry)) }
func (h *entryHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
func topKFrequentContainerHeap(nums []int, k int) []int {
m := countFreq(nums)
h := &entryHeap{}
heap.Init(h)
for v, c := range m {
heap.Push(h, entry{v, c})
if h.Len() > k {
heap.Pop(h)
}
}
res := make([]int, 0, k)
for h.Len() > 0 {
res = append(res, heap.Pop(h).(entry).val)
}
return res
}
// equalIgnoreOrder 忽略顺序比较两个 []int。
func equalIgnoreOrder(a, b []int) bool {
if len(a) != len(b) {
return false
}
ca := append([]int(nil), a...)
cb := append([]int(nil), b...)
sort.Ints(ca)
sort.Ints(cb)
return reflect.DeepEqual(ca, cb)
}
func TestTopKFrequent(t *testing.T) {
cases := []struct {
name string
nums []int
k int
want []int
}{
{"basic", []int{1, 1, 1, 2, 2, 3}, 2, []int{1, 2}},
{"single", []int{1}, 1, []int{1}},
{"tie", []int{1, 2}, 2, []int{1, 2}}, // 频率相同,都可返回
{"negative", []int{4, 1, -1, 2, -1, 2, 3}, 2, []int{-1, 2}},
{"duplicate candidates", []int{3, 0, 1, 0}, 1, []int{0}},
}
solvers := []struct {
name string
fn func([]int, int) []int
}{
{"heap", topKFrequentHeap},
{"bucket", topKFrequentBucket},
{"container-heap", topKFrequentContainerHeap},
}
for _, s := range solvers {
t.Run(s.name, func(t *testing.T) {
for _, c := range cases {
// 复制 nums 防止解法修改原切片
nums := append([]int(nil), c.nums...)
got := s.fn(nums, c.k)
if !equalIgnoreOrder(got, c.want) {
t.Errorf("%s/%s: got %v, want %v", s.name, c.name, got, c.want)
}
}
})
}
}