【问 chatGPT 】如何计算 perfect hash

作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!


假定已知存在 N 个不同的值,如何设计一个 hash 函数,使得这些值在 hashtable 中严格的实现 O(1) 时间复杂度的查询?
实际上,很多编译器把 switch 的很多个分支上的常量,计算出一个 perfect hash,然后运行期的代码就可以使用 O(1) 的时间复杂度来跳转,从而提升性能。

有这样一些场景就可以使用 perfect hash 来优化:例如配置项,通常可能是 1 分钟加载一次,然后就不再变更,并且在程序中高频使用。
为此,就可以为大量的配置值计算 perfect hash,以便在运行期获得最好的性能。

下面是一个字符串计算 perfect hash 的例子:

package main

import (
	"fmt"
)

// 一个简单的 64bit hash(FNV1a 的变体,带 seed)
// 真实项目里你可以换成 xxhash、siphash 等更强的 hash。
func hashStringWithSeed(s string, seed uint64) uint64 {
	const (
		offset64 = 14695981039346656037
		prime64  = 1099511628211
	)

	h := offset64 ^ seed
	for i := 0; i < len(s); i++ {
		h ^= uint64(s[i])
		h *= prime64
	}
	return h
}

// BuildPerfectHash 尝试为给定的 keys 找到“无碰撞”的一组 hash 值。
// 返回:
//   - hashes: 与 keys 等长的 []uint64,每个 key 一个 hash 值
//   - iter:   第几轮 seed 尝试才成功(从 1 开始)
//
// 注意:
//   - 这个例子只是演示 perfect hash 的“构造过程”,不是 minimal perfect hash。
//   - 如果 key 数量巨大,穷举 seed 的做法不一定高效,例子仅说明原理。
func BuildPerfectHash(keys []string) (hashes []uint64, iter int) {
	if len(keys) == 0 {
		return nil, 0
	}

	// 从 seed = 1 开始一轮一轮尝试
	for seed := uint64(1); ; seed++ {
		iter++
		seen := make(map[uint64]int, len(keys))
		hashes = make([]uint64, len(keys))

		collision := false
		for i, k := range keys {
			h := hashStringWithSeed(k, seed)
			if prevIdx, ok := seen[h]; ok {
				// 发现碰撞,当前 seed 失败,换下一轮
				_ = prevIdx // 这里你也可以记录一下是哪个 key 冲突
				collision = true
				break
			}
			seen[h] = i
			hashes[i] = h
		}

		if !collision {
			// 当前 seed 对所有 key 都无碰撞,认为构造成功
			return hashes, iter
		}
		// 否则继续下一轮 seed++
	}
}

func main() {
	keys := []string{
		"GET /health",
		"POST /login",
		"POST /logout",
		"GET /user",
		"PUT /user",
	}

	hashes, iter := BuildPerfectHash(keys)

	fmt.Printf("构造成功,使用的迭代轮次 n = %d\n", iter)
	for i, k := range keys {
		fmt.Printf("key[%d] = %-15q  hash = %d (0x%x)\n",
			i, k, hashes[i], hashes[i])
	}
}

posted on 2026-01-02 17:10  ahfuzhang  阅读(0)  评论(0)    收藏  举报