哈希表
今天是7.20,是计划的截止日期。前几天一直在玩滑板拖着没写博客没学习,今天下午又学习了一遍哈希表的知识,按计划该写博文完成哈希表的学习。
哈希表(hash table)又称为散列表,是一种对实体进行索引的数据结构。它的特点是查询速度快,一个拥有好的哈希函数的哈希表,进行查找时可以实现均摊常量时间O(1)的时间复杂度,这无疑是很快速的查找速度。比方说,数组本身就是最简单的一种哈希表,根据数组下标可以快速找到下标对应的数组元素。
对于哈希表,根据解决冲突的形式不同,有开散列和闭散列两种形式。这里的“开、闭”可以形象地认为是,主表状态是发散(开散列)的还是一维线性结构(闭散列)。
有了对哈希表的大概了解,下面再详细介绍一下哈希表的基本组成部分。
首先,哈希表由键-值组成(key-value)。键是实体被索引时的标识符,值是被索引的实体。举下面一个例子对键和值进行说明:
《红楼梦》 ————> 文学类,
在以上的例子中,文学类即是键,而《红楼梦》则是值。一般使用数组元素存储值,使用数组下标表示键。
其次,由值转化为键的过程,需要使用哈希函数,哈希函数是一种将值映射到键的规则与方法。哈希函数可以理解为值与键的对应关系。输入一个值,根据哈希函数,可以求得一个对应的键。一个好的哈希函数,会让将不同的值尽可能映射成不同的键。举下面一个例子对哈希函数进行说明:
已知值:apple、bed、egg;
已知哈希函数:根据实体首字母,将a对应1,将b对应2,以此类推……;
那么组成的哈希表如下:
键(数组下标) | 0 | 1 | 2 | 3 | 4 | 5 |
值(实体) | NULL | apple | bed | NULL | NULL | egg |
当然上面这个哈希函数并不好,因为很明显以a开头的单词多了去了,此时如果仍旧使用这个哈希函数,将导致大量冲突的产生,关于冲突的解决我们在第三部分讲解。常用的哈希函数一般会这么操作:首先将值映射到一个整数,再通过对一个大质数取模来映射到一定范围内的整数。举下面一个例子进行说明:
cba ————> 2 * 260 + 1 * 261 + 0 * 262 = 28,
28 % 1000007 = 28,
经过这两步,就完成了一个将值映射到键的操作。
第三,在第二部分我们简单介绍了冲突的情况,也就是不同的值恰好被映射到同一个键时怎么办。在这个部分,我们来探究如何解决冲突。解决冲突一般而言有两种办法:开散列、闭散列,这两个概念在第一部分提到过。
开散列,是使用链表来存储每一个键对应的所有的值,值得注意的是,此时主表用来存储链表头而不存储具体的值。这是一种类似邻接表的结构,也可以使用数组模拟链表。
闭散列,是对冲突的键按规则存储值存储的下标,主表可以直接存储具体的值,并且每个下标只能存储一个。还需要一个探测函数,当不同的值发生冲突时,使用探测函数查找下一个值,一个好的探测函数可以尽快解决冲突。最简单的探测函数如下:
① h(p) = p + 1 (p < k)
② h(p) = 0 (p = k)。
闭散列相对于开散列,简单直接,不需要实现链表。但是也有其自身的缺点,比如说会增加整体冲突的概率,并且需要用标记删除来代替真正的删除。
最后,谈一下哈希表的常见运用,如下:
- 模拟映射关系,比如说DNS解析,就是将网址映射到IP地址。
- 防止重复。
- 缓存/记住数据。