哈希表
哈希表 又称散列表 通过建立key与value的映射 实现高效的元素查询 在哈希表中进行增删查改的时间复杂度都是O(1) 非常高效。
哈希表常用操作
哈希表的常见操作包括:初始化、查询操作、添加键值对和删除键值对等
/* 初始化哈希表 */
hmap := make(map[int]string)
/* 添加操作 */
// 在哈希表中添加键值对 (key, value)
hmap[12836] = "小李"
hmap[15937] = "小王"
hmap[16750] = "小陈"
hmap[13276] = "小张"
hmap[10583] = "小赵"
/* 查询操作 */
// 向哈希表中输入键 key ,得到值 value
name := hmap[15937]
/* 删除操作 */
// 在哈希表中删除键值对 (key, value)
delete(hmap, 10583)
哈希表有三种常用的遍历方式:遍历键值对 遍历键 遍历值
/* 遍历哈希表 */
// 遍历键值对 key->value
for key, value := range hmap {
fmt.Println(key, "->", value)
}
// 单独遍历键 key
for key := range hmap {
fmt.Println(key)
}
// 单独遍历值 value
for _, value := range hmap {
fmt.Println(value)
}
哈希表简单实现
我们简单实现一个哈希表,仅用一个数组来实现哈希表
哈希表中,将数组中的每个空位称为桶(bucket),每个桶可存储一个键值对。查询操作就是找到 key 对应的桶,并在桶中获取 value 。
如何基于 key 定位对应的桶呢?这是通过哈希函数(hash function)实现的。
输入一个 key ,我们可以通过哈希函数得到该 key 对应的键值对在数组中的存储位置。
哈希函数的计算过程分为以下两步。
- 通过某种哈希算法
hash()计算得到哈希值。 - 将哈希值对桶数量(数组长度)
capacity取模,从而获取该key对应的数组索引index。
下面用代码实现了一个简单哈希表
/* 键值对 */
type pair struct {
key int
val string
}
/* 基于数组实现的哈希表 */
type arrayHashMap struct {
buckets []*pair
}
/* 初始化哈希表 */
func newArrayHashMap() *arrayHashMap {
// 初始化数组,包含 100 个桶
buckets := make([]*pair, 100)
return &arrayHashMap{buckets: buckets}
}
/* 哈希函数 */
func (a *arrayHashMap) hashFunc(key int) int {
index := key % 100
return index
}
/* 查询操作 */
func (a *arrayHashMap) get(key int) string {
index := a.hashFunc(key)
pair := a.buckets[index]
if pair == nil {
return "Not Found"
}
return pair.val
}
/* 添加操作 */
func (a *arrayHashMap) put(key int, val string) {
pair := &pair{key: key, val: val}
index := a.hashFunc(key)
a.buckets[index] = pair
}
/* 删除操作 */
func (a *arrayHashMap) remove(key int) {
index := a.hashFunc(key)
// 置为 nil ,代表删除
a.buckets[index] = nil
}
/* 获取所有键对 */
func (a *arrayHashMap) pairSet() []*pair {
var pairs []*pair
for _, pair := range a.buckets {
if pair != nil {
pairs = append(pairs, pair)
}
}
return pairs
}
/* 获取所有键 */
func (a *arrayHashMap) keySet() []int {
var keys []int
for _, pair := range a.buckets {
if pair != nil {
keys = append(keys, pair.key)
}
}
return keys
}
/* 获取所有值 */
func (a *arrayHashMap) valueSet() []string {
var values []string
for _, pair := range a.buckets {
if pair != nil {
values = append(values, pair.val)
}
}
return values
}
/* 打印哈希表 */
func (a *arrayHashMap) print() {
for _, pair := range a.buckets {
if pair != nil {
fmt.Println(pair.key, "->", pair.val)
}
}
}
哈希冲突与扩容
哈希函数的作用是将所有 key 构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,理论上一定存在“多个输入对应相同输出”的情况。们将这种多个输入对应同一输出的情况称为哈希冲突。
一般来说,哈希表容量n越大,多个 key 被分配到同一个桶中的概率就越低,冲突就越少。因此,我们可以通过扩容哈希表来减少哈希冲突。
类似于数组扩容,哈希表扩容需将所有键值对从原哈希表迁移至新哈希表,非常耗时;并且由于哈希表容量 capacity 改变,我们需要通过哈希函数来重新计算所有键值对的存储位置,这进一步增加了扩容过程的计算开销。为此,通常会预留足够大的哈希表容量,防止频繁扩容。
负载因子是哈希表的一个重要概念,其定义为哈希表的元素数量除以桶数量,用于衡量哈希冲突的严重程度,也常作为哈希表扩容的触发条件。例如在 Java 中,当负载因子超过0.75时,系统会将哈希表扩容至原先的2倍。