Files
leetcode/top-100-liked/75/leetcode_test.go
T
2026-07-23 16:56:50 +08:00

233 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package top100liked
import (
"container/heap"
"reflect"
"sort"
"testing"
)
// 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。下面三种解法都基于此。
// countFreq 统计每个元素出现次数。
func countFreq(nums []int) map[int]int {
m := make(map[int]int)
for _, v := range nums {
m[v]++
}
return m
}
// 解法一:最小堆 — 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
}
}
}
// 下沉:从 i 开始,与较小子比较,比子大则交换向下。n 是有效堆大小
func (h *minHeap) siftDown(i, n int) {
for {
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 < n && h.data[r].cnt < h.data[smallest].cnt {
smallest = r
}
if smallest == i {
break
}
h.data[smallest], h.data[i] = h.data[i], h.data[smallest]
i = smallest
}
}
// push:追加末尾 + 上浮
func (h *minHeap) push(e entry) {
idx := len(h.data)
h.data = append(h.data, e)
h.siftUp(idx)
}
// popMin 弹出堆顶(最小 cnt):用末尾覆盖堆顶,缩容,下沉
func (h *minHeap) popMin() entry {
if len(h.data) == 0 {
return entry{}
}
ret := h.data[0]
h.data[0] = h.data[len(h.data)-1]
h.data = h.data[:len(h.data)-1]
if len(h.data) > 0 {
h.siftDown(0, len(h.data))
}
return ret
}
func (h *minHeap) len() int { return len(h.data) }
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()
}
}
res := make([]int, 0, k)
for h.len() > 0 {
res = append(res, h.popMin().val)
}
return res
}
// 解法二:桶排序 — 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)
}
}
})
}
}