哈希表 - 映射、集合

124 阅读4分钟

Hash Table

我们通常使用的 Map,Set 结构底层基本都是用 Hash 表来实现的,当然还有些会用到二叉树来实现。**哈希表也叫散列表,是根据关键码值(key value)而直接进行访问的数据结构。它通过把要存储的值映射到表中的一个位置(key 或者 index)来访问记录,以加快查找的速度。**这个映射函数叫散列函数(Hash Function),存放记录的数组叫做哈希表(或散列表)。

工程实践

根据一个key(比如人名)来存储值的一些场景

  • 电话号码薄
  • 用户信息表
  • 缓存 (LRU Cache)
  • 键值对存储 (比如Redis)

Hash Function

我们首先来看下哈希表中,映射函数Hash Fuction做了什么

我们有一个string需要存储,它的值为 lies, 那如何存呢,方块代表哈希函数,lies传入后返回一个下标,如图所示返回一个整数9,这样存下即可。

有同学可能会问,lies 下标为何对应9呢,有什么规律么,这就是所谓的 Hash 函数的处理,这个 Hash 函数有很多种,我们在这里列举一个简单的 Hash 函数,就是把每一个字符的 ASCII 码加在一起,然后再模上一个数,比如模上10,429 % 10 得到9。这就是一个简单的 Hash 函数,现实中一些 Hash 函数往往比较复杂,而且 Hash 函数选得好的话,可以让这些数值尽量的分散,而不会发生所谓的碰撞,因为这里的话,lies 算出来得9,那么其他字符也有可能为9,如下图:

也就是说,对于不同的要存储的数据,它经过 Hash 函数之后,如果得到一个相同的值,这种情况就叫做 Hash 碰撞

如果9这个位置要存多个元素,我们要做的事情也比较简单,可以依次往下面存,占别人的位置。当然还有一种更好的办法或者在工程上常用的一种方式是再增加一个维度,这个位置不止存一个数了,也就是从这里开始,拉出来一个链表,这样的方法叫做拉链式解决冲突法。原本需要 O(1) 复杂度的查询,最坏的情况下: 如果发生了很多 Hash 碰撞,它的查询时间就要遍历链表,如果链表很长,它就会效率退化,退化到所谓的 O(n) 级别。好的情况下: 如果设计的很好的话,Hash 函数碰撞的几率很小,所以在平均时刻的话,我们可以认为,整个 Hash 表的查询是 O(1)。

借助下图的完整结构,好好体会下。

Hash 表的时间复杂度

我们发现,它的查询、添加、删除大部分都是 O(1) 的时间复杂度,但是在最坏的情况下,比如哈希函数选的非常不好,或者哈希表的整个 Size 太小了,就导致经常发生冲突,一冲突的话当前存储的值就会退化一个链表了,这个时候,它的时间复杂度就是 O(n) 了。现在计算机的内存越来越大,Hash 表可以开的很大很大,同时随着哈希函数的不断优化,一般来说,我们都可以认为哈希表在正常的情况下,就是 O(1) 的时间复杂度进行查询、添加和删除元素。

Map 和 Set 结构

真正的工程中,我们使用的就不是 hash 表了,而是在它的基础上抽象出来的,使用比较多的就是 Map 和 Set。

Map: key-value对,key不重复

  • new HasMap() / new TreeMap()
  • map.Set(key, val)
  • map.get(key)
  • map.has(key)
  • map.size()
  • map.clear()

Set: 不重复元素的集合

  • new HashSet() / new TreeSet()
  • set.add(value)
  • set.delete(value)
  • set.has(value)

注意:js中对象并不是严格意义上的hash映射 JS中 Object 作为 Hash 表使用避坑指北