哈希表

58 阅读4分钟

哈希表

哈希表 又称散列表 通过建立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倍。