← 返回文章列表

Go Hot100:哈希三题的 ACM 写法

把两数之和、字母异位词分组和最长连续序列改写成可独立运行的 Go 程序,说明输入输出与边界情况。

LeetCode 一般已经给好函数签名,我们只要写算法。换成 ACM 写法,就得自己读输入、调用函数、输出答案。算法没变,容易出问题的是这些外面的东西。

这里先把哈希三题改成可以独立运行的程序:两数之和、字母异位词分组、最长连续序列,本篇覆盖这三题。

下面的输入输出格式为本地练习自行约定;LeetCode 仍按函数式判题。每段代码单独保存为 main.go,用 go run main.go 运行。

1、两数之和

原题:Two Sum。

思路:遍历到 x 的时候,先查 target-x 是否已经出现。找到就返回这两个位置;没找到再保存当前元素。先查再存,才能避免重复使用同一个位置。

这里的 map 查找按平均 O(1) 计算,整个算法平均 O(n),额外空间 O(n)。

输入约定:第一行是元素数量 n 和目标值,后面读 n 个整数。输出从 0 开始的两个下标。原题保证有解;这里为了本地测试,把无解情况输出成 -1 -1。

4 9
2 7 11 15

输出:

0 1
package main

import (
	"bufio"
	"fmt"
	"os"
)

func twoSum(nums []int, target int) (int, int) {
	seen := make(map[int]int, len(nums))
	for i, x := range nums {
		if j, ok := seen[target-x]; ok {
			return j, i
		}
		seen[x] = i
	}
	return -1, -1
}

func main() {
	in := bufio.NewReader(os.Stdin)
	out := bufio.NewWriter(os.Stdout)
	defer out.Flush()
	var n, target int
	if _, err := fmt.Fscan(in, &n, &target); err != nil || n < 0 {
		return
	}
	nums := make([]int, n)
	for i := range nums {
		if _, err := fmt.Fscan(in, &nums[i]); err != nil {
			return
		}
	}
	i, j := twoSum(nums, target)
	fmt.Fprintln(out, i, j)
}

拿 3 3、两个 0、负数和无解情况再测一下,可以检查是不是错误地用了一次元素两遍。

2、字母异位词分组

原题:Group Anagrams。

字母异位词的字母和数量相同,只是顺序不同。按原题的小写英文字母范围,可以用 [26]int 记录每个字母出现的次数。数组可以做 Go map 的键,这样就不需要给每个单词排序了。

分组平均时间是 O(nL),L 是单词最大长度。下面为了让本地输出稳定,会给每组和所有组排序;这部分会额外增加排序开销,不能把整个带格式化输出的程序也说成只有 O(nL)。

输入约定:第一行是单词数量,之后每个单词占一行。空行表示空字符串,所以这里用 Scanner 逐行读,不能只用 Fscan 读单词。

6
eat
tea
tan
ate
nat
bat

输出为 JSON,方便保留空字符串和分组边界:

[["ate","eat","tea"],["bat"],["nat","tan"]]
package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"sort"
	"strconv"
	"strings"
)

func groupAnagrams(words []string) [][]string {
	groups := make(map[[26]int][]string)
	for _, word := range words {
		var key [26]int
		for _, ch := range word {
			key[ch-'a']++
		}
		groups[key] = append(groups[key], word)
	}
	result := make([][]string, 0, len(groups))
	for _, group := range groups {
		sort.Strings(group)
		result = append(result, group)
	}
	sort.Slice(result, func(i, j int) bool {
		return result[i][0] < result[j][0]
	})
	return result
}

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	if !scanner.Scan() {
		return
	}
	n, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
	if err != nil || n < 0 {
		return
	}
	words := make([]string, n)
	for i := range words {
		if !scanner.Scan() {
			return
		}
		words[i] = scanner.Text()
		for _, ch := range words[i] {
			if ch < 'a' || ch > 'z' {
				fmt.Fprintln(os.Stderr, "仅支持小写英文字母和空字符串")
				return
			}
		}
	}
	if err := json.NewEncoder(os.Stdout).Encode(groupAnagrams(words)); err != nil {
		fmt.Fprintln(os.Stderr, err)
	}
}

输入 1 后再给一行空行,结果应为 [[""]]。只剩一个普通单词时也要能正确输出。这两种情况很容易在读输入时被漏掉。

3、最长连续序列

原题:Longest Consecutive Sequence。

先把数字放到集合里。如果 x-1 存在,说明 x 不是一段连续序列的起点,跳过它。只有起点才继续向后数。

这样每个数字不会反复作为起点扫描。按哈希集合平均 O(1) 的查找计算,平均时间 O(n),额外空间 O(n)。数值范围沿用原题的 [-10^9, 10^9],不是任意超出 int 边界的整数。

输入约定:先读元素数量,再读对应的整数。数组为空时输出 0。

6
100 4 200 1 3 2

输出:

4
package main

import (
	"bufio"
	"fmt"
	"os"
)

func longestConsecutive(nums []int) int {
	set := make(map[int]bool, len(nums))
	for _, x := range nums {
		set[x] = true
	}
	best := 0
	for x := range set {
		if set[x-1] {
			continue
		}
		length := 1
		for next := x + 1; set[next]; next++ {
			length++
		}
		if length > best {
			best = length
		}
	}
	return best
}

func main() {
	in := bufio.NewReader(os.Stdin)
	var n int
	if _, err := fmt.Fscan(in, &n); err != nil || n < 0 {
		return
	}
	nums := make([]int, n)
	for i := range nums {
		if _, err := fmt.Fscan(in, &nums[i]); err != nil {
			return
		}
	}
	fmt.Println(longestConsecutive(nums))
}

重复数字不会增加序列长度;输入 [1, 0, 1, 2] 的长度还是 3。还可以测空数组、全是同一个数和包含负数的连续段。

4、ACM 写法主要多考虑什么?

算法写完以后,单独检查输入输出:读的是 token 还是整行,题目有没有多组数据,能否出现空字符串,下标从 0 还是 1 开始,输出是否要求固定顺序。

bufio.Reader 配合 fmt.Fscan 比较容易写;如果数据量非常大,再根据评测情况换更快的整数读取器。bufio.Scanner 默认有 token 长度限制,读长文本时需要设置 Buffer,不能把当前的短单词示例直接套到任意大文件。

接口说明:fmt.Fscan、bufio.Scanner。

关于这篇文章的日期

旧日期 2025-11-16 与文章发布记录不符,现按旧站可确认的首次发布日 2026-01-20 整理。实际创作日期尚无法确认。 旧页仅有占位标题,正文于 2026-10-06 补充。