散列表
散列表也叫哈希表(hash table),这种数据结构是key-value的映射集合。对于某一个key,散列表可以在接近O(1)的时间内进行读写操作。那么散列表是如何根据key高效的找到对应的value呢?
哈希函数
散列表在内存中的本质是数组,因为数组相对链表结构来说,读取的效率更高。但是数组是通过下标index来访问组内元素的,而散列表是根据key来访问的。而哈希函数的作用就是把key通过某种方法转换成index。
实现原理
不同的语言,哈希函数的实现方法不一样。比如Java中,每个对象都有自己的hashCode,且是一个整形变量,转换成下标最简单的方式就是按照数组的长度进行取模运算。
index = hashCode(key) % Array.length
这里用取模运算便于理解一些,实际上Java中的一些实现并没有直接使用取模运算,比如JDK是利用了位运算的方式来优化性能。其它方法:
- 直接地址法:以关键字的某个线性函数值为哈希地址(
index),可以表示为hash(K)=aK+C;优点是不会产生哈希冲突,缺点是空间复杂度可能会较高,适用于元素较少的情况 - 数字分析法:该方法是取数据元素关键字中某些取值较均匀的数字来作为哈希地址(
index)的方法,这样可以尽量避免冲突,但是该方法只适合于所有关键字已知的情况,对于想要设计出更加通用的哈希表并不适用 - 平方求和法:对当前字串转化为Unicode值,并求出这个值的平方,去平方值中间的几位为当前数字的哈希地址(
index),具体取几位要取决于当前散列表的大小。 - 分段求和法:根据当前散列表的位数把所要插入的数值分成若干段,把若干段进行相加,舍去最高位结果就是这个值的哈希地址(
index)。
哈希冲突
数组的长度是有限的,当被转换的key有一定数量时,可能会存在两个key转换后,得到相同的哈希地址index,这种情况就叫做哈希冲突
解决方法
- 开放寻址法:原理很简单,当一个
key通过获得的对应数组下标index即哈希地址已被占用时,可以顺延向后一位,判断是否有空位,以此类推。寻址的方式有很多种,并不一定只是简单的寻找当前元素的后一位。 - 链表法也叫拉链法:原理就是使用数组与链表的组合的数据结构。即每个数组元素就是一个链表结构,发生哈希冲突时,可以通过元素链表的特性,把冲突的
key指向下一个节点地址。举个例子:key为foo经过哈希函数转换后对应数组下标index为3,在数组元素是链表结构的前提下,此时链表头节点为foo,而当第二个key为bar经过哈希函数转换后同样也是index=3,那么此时插入到元素的链表结构中,foo的next节点指向bar
散列表的读写过程
- 写
- 通过哈希函数,把
key转换成数组下标3 - 如果数组下标3对应位置没有元素,就把数据写入
- 如果已经有了,用上面方法解决哈希冲突后再写入即可
- 通过哈希函数,把
- 读
- 通过哈希函数,把
key=foo转换成数组下标3 - 找到数组下标3的元素,如果这个元素是链表结构,还得判断这个元素的
key值是否为目标值foo,如果不相等,则可以顺着链表继续往下找,找到key为foo相匹配的节点
- 通过哈希函数,把
参考
- JavaScript 哈希表
- 数据结构hash table
- 《漫画算法 小灰的算法之旅》