This commit is contained in:
2026-07-18 23:06:46 +08:00
parent 3bd18ddaf2
commit 4fa78d4c00
5 changed files with 719 additions and 0 deletions
+197
View File
@@ -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。
+4
View File
@@ -0,0 +1,4 @@
package main
func main() {
}
+287
View File
@@ -0,0 +1,287 @@
package main
import (
"fmt"
"testing"
)
// QuickSortHoare 使用 Hoare 双指针分区:l 主动向右找 >= pivotr 主动向左找 <= 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 // 全相等,无需排
}
// 桶数取 nidx = (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()
}
}
+48
View File
@@ -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)
}
+183
View File
@@ -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 升序(堆顶最小)
// - 遍历 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
}
// 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)
}
}
})
}
}