u
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
package main
|
||||
|
||||
// input-scan-cheatsheet.go
|
||||
// ACM/笔试 输入解析速查表(Go)。所有模板均经本机 go1.26.3 实测通过。
|
||||
// 来源:掘金 ACM 模板(starine, 2022) + CSDN fmt/bufio 对比 + 本机实测。
|
||||
//
|
||||
// 本文件与 main.go 同属 package main,但所有模板均为独立函数,
|
||||
// 不与 main.go 的 main() 冲突。直接在 main.go 里调用任一 demoX() 即可验证。
|
||||
//
|
||||
// ───────────────────────── 场景索引 ─────────────────────────
|
||||
// demo1 EOF 多组,每组两个数求和 fmt.Scan
|
||||
// demo2 先读组数 t,再 t 组每组两个数 fmt.Scan
|
||||
// demo3 特殊值结束(0 0 停) fmt.Scan
|
||||
// demo4 先读 n 再读 n 个元素(行布局无关) fmt.Scan ★最省心
|
||||
// demo5 逐行读 + 行内不定个数求和 bufio.Scanner
|
||||
// demo6 先 n 再 n 个元素(Scanner 鲁棒版) bufio.Scanner
|
||||
// demo7 t 组,每组先 n 再 n 个数 bufio.Scanner
|
||||
// demo8 大数据量:Scanner 读 + Writer 输出 bufio 双向缓冲
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ──────────────────────── fmt.Scan 系列 ────────────────────────
|
||||
|
||||
// demo1: 多组数据,每组一行两个整数,读到 EOF 结束。
|
||||
// 输入示例: "1 2\n3 4\n5 7\n" → 3 7 12
|
||||
// 关键:fmt.Scan 把所有空白符(空格/换行/tab)当分隔符,无视行边界。
|
||||
// n 是成功填入的变量个数;n==0 表示 EOF。
|
||||
func demo1() {
|
||||
var a, b int
|
||||
for {
|
||||
n, _ := fmt.Scan(&a, &b)
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
fmt.Println(a + b)
|
||||
}
|
||||
}
|
||||
|
||||
// demo2: 第一行组数 t,接下来 t 行每行两个整数。
|
||||
// 输入示例: "2\n1 2\n3 4\n" → 3 7
|
||||
func demo2() {
|
||||
var t, a, b int
|
||||
fmt.Scan(&t)
|
||||
for i := 0; i < t; i++ {
|
||||
fmt.Scan(&a, &b)
|
||||
fmt.Println(a + b)
|
||||
}
|
||||
}
|
||||
|
||||
// demo3: 多组数据,读到 0 0 结束(特殊哨兵)。
|
||||
// 输入示例: "1 2\n3 4\n0 0\n" → 3 7
|
||||
// 关键:判停条件由题目给定,与 EOF 无关。仍用 fmt.Scan 读取。
|
||||
func demo3() {
|
||||
var a, b int
|
||||
for {
|
||||
fmt.Scan(&a, &b)
|
||||
if a == 0 && b == 0 {
|
||||
break
|
||||
}
|
||||
fmt.Println(a + b)
|
||||
}
|
||||
}
|
||||
|
||||
// demo4 ★最省心模板:先读 n,再读 n 个元素。行布局无关(挤一行/每元素一行/乱排都行)。
|
||||
// 输入1 "5 1 2 3 4 5" 输入2 "5\n1 2 3 4 5" 输入3 "5\n1\n2\n3\n4\n5" 全部 → [1 2 3 4 5]
|
||||
// 关键:fmt.Scan 无视行边界,只按空白切分。naive Scanner 写法在"n 与元素挤一行"时会崩,见 demo6 对比。
|
||||
func demo4() {
|
||||
var n int
|
||||
fmt.Scan(&n)
|
||||
a := make([]int, n)
|
||||
for i := 0; i < n; i++ {
|
||||
fmt.Scan(&a[i])
|
||||
}
|
||||
fmt.Println(a)
|
||||
}
|
||||
|
||||
// ──────────────────────── bufio.Scanner 系列 ────────────────────────
|
||||
|
||||
// newScanner 返回带 1MB 缓冲的 Scanner,避免单行过长触发默认 64KB 上限。
|
||||
func newScanner() *bufio.Scanner {
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
return sc
|
||||
}
|
||||
|
||||
// demo5: 多组数据,每行不定个数整数求和。逐行读 + strings.Fields 切分。
|
||||
// 输入示例: "1 2 3\n4 5\n6 7 8 9\n" → 6 9 30
|
||||
// 关键:strings.Fields 按任意空白符切,自动跳过连续空白,不会产生空元素。
|
||||
// 优先用 Fields 而非 strings.Split(s, " ")——后者遇连续空格会产生空字符串。
|
||||
func demo5() {
|
||||
sc := newScanner()
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var sum int
|
||||
for _, s := range strings.Fields(line) {
|
||||
v, _ := strconv.Atoi(s)
|
||||
sum += v
|
||||
}
|
||||
fmt.Println(sum)
|
||||
}
|
||||
}
|
||||
|
||||
// demo6: 先读 n 再读 n 个元素(Scanner 鲁棒版)。兼容"n 与元素挤一行"的情况。
|
||||
// 关键:naive 写法(sc.Scan 一行当 n 去解析)在 "5 1 2 3 4 5" 上会 Atoi 失败返回 0。
|
||||
// 鲁棒做法:把第一行用 Fields 切开,第一个 token 是 n,余下的直接进数组,不够再读后续行。
|
||||
func demo6() {
|
||||
sc := newScanner()
|
||||
sc.Scan()
|
||||
tokens := strings.Fields(sc.Text())
|
||||
n, _ := strconv.Atoi(tokens[0])
|
||||
a := make([]int, 0, n)
|
||||
for _, s := range tokens[1:] {
|
||||
v, _ := strconv.Atoi(s)
|
||||
a = append(a, v)
|
||||
}
|
||||
for len(a) < n && sc.Scan() {
|
||||
for _, s := range strings.Fields(sc.Text()) {
|
||||
v, _ := strconv.Atoi(s)
|
||||
a = append(a, v)
|
||||
}
|
||||
}
|
||||
fmt.Println(a)
|
||||
}
|
||||
|
||||
// demo7: t 组,每组先读 n 再读 n 个整数求和。
|
||||
// 输入示例: "2\n3 1 2 3\n2 4 5\n" → 6 9
|
||||
// 组合模式:外层组数用 Scan,内层元素用 Scanner 按行 + Fields 收集。
|
||||
func demo7() {
|
||||
sc := newScanner()
|
||||
sc.Scan()
|
||||
t, _ := strconv.Atoi(strings.TrimSpace(sc.Text()))
|
||||
for i := 0; i < t; i++ {
|
||||
sc.Scan()
|
||||
tokens := strings.Fields(sc.Text())
|
||||
n, _ := strconv.Atoi(tokens[0])
|
||||
a := make([]int, 0, n)
|
||||
for _, s := range tokens[1:] {
|
||||
v, _ := strconv.Atoi(s)
|
||||
a = append(a, v)
|
||||
}
|
||||
for len(a) < n && sc.Scan() {
|
||||
for _, s := range strings.Fields(sc.Text()) {
|
||||
v, _ := strconv.Atoi(s)
|
||||
a = append(a, v)
|
||||
}
|
||||
}
|
||||
sum := 0
|
||||
for _, v := range a {
|
||||
sum += v
|
||||
}
|
||||
fmt.Println(sum)
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────── 大数据量模板 ────────────────────────
|
||||
|
||||
// demo8: 大数据量(>10^5 行)。Scanner 读 + Writer 输出,双向缓冲。
|
||||
// 输入示例: 大量行,每行一个整数,输出每个的 2 倍。
|
||||
// 关键:fmt.Print 系列在 10^5 级输出时会明显变慢;bufio.NewWriter + Flush 提速一个量级。
|
||||
// defer w.Flush() 必须有,否则缓冲区内容不会写出。
|
||||
func demo8() {
|
||||
sc := newScanner()
|
||||
w := bufio.NewWriter(os.Stdout)
|
||||
defer w.Flush()
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
v, _ := strconv.Atoi(strings.TrimSpace(line))
|
||||
fmt.Fprintln(w, v*2)
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────── 坑速查 ────────────────────────
|
||||
// 1. fmt.Scan 读不全时,未填的变量保留上轮值(不会清零)。
|
||||
// → 判停用 n < 预期个数,别信任未读到的变量。
|
||||
// 2. naive Scanner(整行 Atoi 当 n)在"n 与元素挤一行"时返回 n=0。
|
||||
// → 用 strings.Fields 切开第一行(见 demo6)。
|
||||
// 3. strings.Split(s, " ") 遇连续空格产生空字符串元素。
|
||||
// → 优先 strings.Fields(s)。
|
||||
// 4. bufio.Scanner 默认 64KB 上限,超长行会读不全。
|
||||
// → sc.Buffer(make([]byte, 1024*1024), 1024*1024)。
|
||||
// 5. bufio.NewWriter 必须 Flush,否则输出丢失。
|
||||
// → defer w.Flush()。
|
||||
// 6. fmt.Scanln 遇换行即停,不跨行;fmt.Scan 无视行边界跨行连读。
|
||||
// → ACM 默认用 fmt.Scan。
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// QuickSortHoare 使用 Hoare 双指针分区:l 主动向右找 >= pivot,r 主动向左找 <= pivot,
|
||||
// 两指针相遇时分区完成。取中间元素作 pivot 避免已排序/逆序退化;
|
||||
// 内层用严格不等号,相等元素也让两指针停下交换,防止全相等数组死循环。
|
||||
func QuickSortHoare(nums []int) {
|
||||
var helper func(start, end int)
|
||||
helper = func(start, end int) {
|
||||
if start >= end {
|
||||
return
|
||||
}
|
||||
pivot := nums[start+(end-start)/2] // 取中间元素,避免已排序/逆序退化
|
||||
l, r := start, end
|
||||
for {
|
||||
for nums[l] < pivot { // l 主动向右找 >= pivot 的
|
||||
l++
|
||||
}
|
||||
for nums[r] > pivot { // r 主动向左找 <= pivot 的
|
||||
r--
|
||||
}
|
||||
if l >= r { // 两指针相遇/交叉,分区完成
|
||||
break
|
||||
}
|
||||
nums[l], nums[r] = nums[r], nums[l]
|
||||
l++
|
||||
r-- // 各进一步,相等元素也会交换,防止全相等数组死循环
|
||||
}
|
||||
helper(start, r)
|
||||
helper(r+1, end)
|
||||
}
|
||||
helper(0, len(nums)-1)
|
||||
}
|
||||
|
||||
// QuickSortLomuto 使用 Lomuto 单向分区:pivot 放 nums[end],i 维护"<= pivot 区"的右边界+1,
|
||||
// j 从左扫到 end-1,遇到 <= pivot 的就与 nums[i] 交换并 i++。
|
||||
// 循环结束后 swap(nums[i], nums[end]) 让 pivot 归位到 i,左半 <= pivot、右半 > pivot。
|
||||
func QuickSortLomuto(nums []int) {
|
||||
var helper func(start, end int)
|
||||
helper = func(start, end int) {
|
||||
if start >= end {
|
||||
return
|
||||
}
|
||||
pivot := nums[end] // 取最后一个作 pivot
|
||||
i := start // i 指向"<= pivot 区"的右边界+1
|
||||
for j := start; j < end; j++ {
|
||||
if nums[j] <= pivot {
|
||||
nums[i], nums[j] = nums[j], nums[i]
|
||||
i++
|
||||
}
|
||||
}
|
||||
nums[i], nums[end] = nums[end], nums[i] // 把 pivot 放到最终位置 i
|
||||
helper(start, i-1)
|
||||
helper(i+1, end)
|
||||
}
|
||||
helper(0, len(nums)-1)
|
||||
}
|
||||
|
||||
func TestQuickSort(t *testing.T) {
|
||||
nums := []int{1, 2, 1, 2, 1, 2, 3, 1, 3, 2}
|
||||
QuickSortHoare(nums)
|
||||
fmt.Printf("Hoare: %+v\n", nums)
|
||||
|
||||
nums2 := []int{1, 2, 1, 2, 1, 2, 3, 1, 3, 2}
|
||||
QuickSortLomuto(nums2)
|
||||
fmt.Printf("Lomuto: %+v\n", nums2)
|
||||
}
|
||||
|
||||
func BubbleSort(nums []int) {
|
||||
for i := 0; i < len(nums); i++ {
|
||||
swapped := false
|
||||
for j := 0; j < len(nums)-1-i; j++ {
|
||||
if nums[j] > nums[j+1] {
|
||||
nums[j], nums[j+1] = nums[j+1], nums[j]
|
||||
swapped = true
|
||||
}
|
||||
}
|
||||
if !swapped {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBubbleSort(t *testing.T) {
|
||||
nums := []int{1, 2, 1, 2, 1, 2, 3, 1, 3, 2}
|
||||
BubbleSort(nums)
|
||||
fmt.Printf("TestBubbleSort: %+v\n", nums)
|
||||
}
|
||||
|
||||
// SelectionSort 选择排序:每轮在 [i, n) 找最小值,与 nums[i] 交换。
|
||||
// 交换次数少(每轮最多一次),但比较次数固定 O(n²),不稳定。
|
||||
func SelectionSort(nums []int) {
|
||||
for i := 0; i < len(nums); i++ {
|
||||
minIdx := i
|
||||
for j := i; j < len(nums); j++ {
|
||||
if nums[j] < nums[minIdx] {
|
||||
minIdx = j
|
||||
}
|
||||
}
|
||||
nums[i], nums[minIdx] = nums[minIdx], nums[i]
|
||||
}
|
||||
}
|
||||
|
||||
// InsertionSort 插入排序:把 nums[i] 往前插到 [0, i) 的正确位置。
|
||||
// 近乎有序时接近 O(n);是很多高级排序在小数组时的收尾段。稳定。
|
||||
func InsertionSort(nums []int) {
|
||||
// for i := 1; i < len(nums); i++ {
|
||||
// for j := i; j > 0; j-- {
|
||||
// if nums[j] < nums[j-1] {
|
||||
// nums[j], nums[j-1] = nums[j-1], nums[j]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
for i := 1; i < len(nums); i++ {
|
||||
key := nums[i]
|
||||
j := i
|
||||
for j > 0 && nums[j-1] > key {
|
||||
nums[j] = nums[j-1] // 往右挪一格
|
||||
j--
|
||||
}
|
||||
nums[j] = key // 落到正确位置
|
||||
}
|
||||
}
|
||||
|
||||
// MergeSort 归并排序:分治——左半排好 + 右半排好,再合并。
|
||||
// 稳定,最坏 O(n log n),需 O(n) 额外空间。合并用双指针。
|
||||
func MergeSort(nums []int) {
|
||||
tmp := make([]int, len(nums)) // 全局临时数组,只分配一次
|
||||
var helper func(start, end int)
|
||||
helper = func(start, end int) {
|
||||
if start >= end {
|
||||
return
|
||||
}
|
||||
mid := start + (end-start)/2
|
||||
helper(start, mid)
|
||||
helper(mid+1, end)
|
||||
// merge [start,mid] 和 [mid+1,end] → tmp → 拷回 nums
|
||||
p1, p2, k := start, mid+1, start
|
||||
for p1 <= mid && p2 <= end {
|
||||
if nums[p1] <= nums[p2] {
|
||||
tmp[k] = nums[p1]
|
||||
p1++
|
||||
} else {
|
||||
tmp[k] = nums[p2]
|
||||
p2++
|
||||
}
|
||||
k++
|
||||
}
|
||||
for p1 <= mid {
|
||||
tmp[k] = nums[p1]
|
||||
p1++
|
||||
k++
|
||||
}
|
||||
for p2 <= end {
|
||||
tmp[k] = nums[p2]
|
||||
p2++
|
||||
k++
|
||||
}
|
||||
copy(nums[start:end+1], tmp[start:end+1]) // 关键:拷回原切片
|
||||
}
|
||||
helper(0, len(nums)-1)
|
||||
}
|
||||
|
||||
// HeapSort 堆排序:建大顶堆,反复把堆顶(最大值)与末尾交换并下沉调整。
|
||||
// 原地、最坏 O(n log n),不稳定。top-K / 优先队列的基础。
|
||||
func HeapSort(nums []int) {
|
||||
n := len(nums)
|
||||
if n <= 1 {
|
||||
return
|
||||
}
|
||||
// siftDown:在 [0, end] 范围内把 nums[i] 下沉到正确位置
|
||||
var siftDown func(i, end int)
|
||||
siftDown = func(i, end int) {
|
||||
for 2*i+1 <= end {
|
||||
l, r := 2*i+1, 2*i+2
|
||||
largest := i
|
||||
if l <= end && nums[l] > nums[largest] {
|
||||
largest = l
|
||||
}
|
||||
if r <= end && nums[r] > nums[largest] {
|
||||
largest = r
|
||||
}
|
||||
if largest == i {
|
||||
break
|
||||
}
|
||||
nums[i], nums[largest] = nums[largest], nums[i]
|
||||
i = largest
|
||||
}
|
||||
}
|
||||
// 阶段 1:建大顶堆,从最后一个父节点往前
|
||||
for i := n/2 - 1; i >= 0; i-- {
|
||||
siftDown(i, n-1)
|
||||
}
|
||||
// 阶段 2:反复把堆顶(最大)与末尾交换,堆范围 -1,修复堆
|
||||
for end := n - 1; end > 0; end-- {
|
||||
nums[0], nums[end] = nums[end], nums[0]
|
||||
siftDown(0, end-1) // 注意:堆范围是 [0, end-1],排除已归位的 nums[end]
|
||||
}
|
||||
}
|
||||
|
||||
// ShellSort 希尔排序:按递减步长分组做插入排序,最后步长为 1 退化为插入排序。
|
||||
// 平均约 O(n^1.3),非稳定。代码短,比插入排序在乱序数据上快很多。
|
||||
func ShellSort(nums []int) {
|
||||
// TODO: 你来写
|
||||
}
|
||||
|
||||
// CountingSort 计数排序:元素为 [0, k] 整数,统计频次后前缀和定位。
|
||||
// O(n+k),k=值域大小。非比较,稳定版本需倒序填充。
|
||||
func CountingSort(nums []int) {
|
||||
// TODO: 你来写(假设元素均 >= 0)
|
||||
}
|
||||
|
||||
// BucketSort 桶排序:按值把元素分到 n 个桶,每桶用 InsertionSort 排好,再依次写回原数组。
|
||||
// 平均 O(n+k),k=桶数;最坏 O(n²)(全挤进一个桶,退化成插入排序)。
|
||||
// 稳定(InsertionSort 稳定)。需 min/max 决定桶映射区间。
|
||||
func BucketSort(nums []int) {
|
||||
n := len(nums)
|
||||
if n <= 1 {
|
||||
return
|
||||
}
|
||||
// 找值域
|
||||
minVal, maxVal := nums[0], nums[0]
|
||||
for _, v := range nums {
|
||||
if v < minVal {
|
||||
minVal = v
|
||||
}
|
||||
if v > maxVal {
|
||||
maxVal = v
|
||||
}
|
||||
}
|
||||
if minVal == maxVal {
|
||||
return // 全相等,无需排
|
||||
}
|
||||
// 桶数取 n;idx = (v-minVal) * n / (maxVal-minVal+1),分母 +1 保证 maxVal 不越界
|
||||
buckets := make([][]int, n)
|
||||
for _, v := range nums {
|
||||
idx := (v - minVal) * n / (maxVal - minVal + 1)
|
||||
buckets[idx] = append(buckets[idx], v)
|
||||
}
|
||||
// 每桶内部插入排序(稳定),再依次拷回原数组
|
||||
k := 0
|
||||
for _, b := range buckets {
|
||||
InsertionSort(b)
|
||||
copy(nums[k:], b)
|
||||
k += len(b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortAlgos(t *testing.T) {
|
||||
cases := [][]int{
|
||||
{1, 2, 1, 2, 1, 2, 3, 1, 3, 2},
|
||||
{3, 2, 1},
|
||||
{5, 4, 3, 2, 1},
|
||||
{2, 1},
|
||||
{1, 3, 2, 3, 1},
|
||||
{10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
|
||||
{1},
|
||||
{},
|
||||
{2, 2, 2, 2},
|
||||
{3, 3, 3, 1, 1, 1, 2, 2, 2},
|
||||
}
|
||||
sorters := []struct {
|
||||
name string
|
||||
fn func([]int)
|
||||
}{
|
||||
// {"SelectionSort", SelectionSort},
|
||||
// {"InsertionSort", InsertionSort},
|
||||
// {"MergeSort", MergeSort},
|
||||
{"HeapSort", HeapSort},
|
||||
{"BucketSort", BucketSort},
|
||||
// {"ShellSort", ShellSort},
|
||||
// {"CountingSort", CountingSort},
|
||||
}
|
||||
for _, s := range sorters {
|
||||
for _, c := range cases {
|
||||
got := append([]int(nil), c...)
|
||||
s.fn(got)
|
||||
fmt.Printf("%s in=%v got=%v\n", s.name, c, got)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package top100liked
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// https://leetcode.cn/problems/kth-largest-element-in-an-array/?envType=study-plan-v2&envId=top-100-liked
|
||||
|
||||
func findKthLargest(nums []int, k int) int {
|
||||
n := len(nums)
|
||||
target := n - k // 第 k 大 = 升序第 n-k 位
|
||||
var quickSelect func(l, r int) int
|
||||
quickSelect = func(l, r int) int {
|
||||
if l == r {
|
||||
return nums[l]
|
||||
}
|
||||
pivot := nums[l+rand.Intn(r-l+1)]
|
||||
// 三路分区:[l..lt-1] < pivot, [lt..gt] == pivot, [gt+1..r] > pivot
|
||||
lt, gt, i := l, r, l
|
||||
for i <= gt {
|
||||
switch {
|
||||
case nums[i] < pivot:
|
||||
nums[lt], nums[i] = nums[i], nums[lt]
|
||||
lt++
|
||||
i++
|
||||
case nums[i] > pivot:
|
||||
nums[gt], nums[i] = nums[i], nums[gt]
|
||||
gt--
|
||||
default:
|
||||
i++
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case target < lt:
|
||||
return quickSelect(l, lt-1)
|
||||
case target > gt:
|
||||
return quickSelect(gt+1, r)
|
||||
default:
|
||||
return pivot
|
||||
}
|
||||
}
|
||||
return quickSelect(0, n-1)
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
findKthLargest([]int{3, 2, 1, 5, 6, 4}, 2)
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package top100liked
|
||||
|
||||
import (
|
||||
"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 升序(堆顶最小)
|
||||
// - 遍历 m:heap.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
|
||||
}
|
||||
|
||||
// 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},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user