一直很好奇为啥哈希表(散列表)查询的速度比数组快,然后自己看了几篇文章,有了一点点自己的理解。
一、散列表
什么是散列表呢?散列表其实是一个数组,通过key映射到这个数组的下标,直接根据下标获取该元素。数组中的每一个元素成为一个箱子。
当我们根据key来查询的时候,首先获取key的哈希值,哈希值是一个整型数,然后这个哈希值 模 数组的长度,得到key在这个数组的下标,这样就不用遍历数组查找key而是直接就拿到了key的下标。但是不同的key可能会计算出相同的哈希值。所以这就需要一个优秀的哈希算法和解决出现生成相同哈希值(散列冲突)的方法。
二、散列算法
散列算法的具体实现我肯定是不知道的,但是一个好的散列算法应该具有以下特点:
- 从哈希值不能反向推导出原始数据(所以哈希算法也叫单向哈希算法);
- 对输入数据非常敏感,哪怕原始数据只修改了一个 Bit,最后得到的哈希值也大不相同;
- 散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小;
- 哈希算法的执行效率要尽量高效,针对较长的文本,也能快速地计算出哈希值。
三、解决散列冲突
解决散列冲突好像比较流行的就是拉链法
和开放寻址法
。
3.1 拉链法
所谓拉链法就是哈希表数组中的每一个箱子都是一个链表,当不同key的哈希值相同,得到相同的下边,就存在同一个箱子的链表中,链表中每个元素保存一个键值对。我们知道链表其实查询速度不是很快,时间复杂度为O(n),但是这个链表的长度其实很短,Java8在链表长度大于8时将链表改为红黑树。总结起来拉链法就是将散列冲突的键值对放在同一个链表中。
3.2 开放寻址法
所谓开放寻址法就是当一个key获取了数组的下标,发现这个箱子已经被别的key占着了,那就往下找(也可能是按照其他规则找空的位置),直到找到空的位置。
四、扩容
最后讲讲扩容,前面说过哈希表是一个数组,那么当插入的key多了之后,冲突就多了起来,这个时候我们就要对数组进行扩容了。有一个成员变量叫做负载因子来决定什么时候需要扩容。负载因子的计算方法是:负载因子 = 键值对数 ÷ 数组的长度,一般来说当负载因子 ≥ 0.72的时候就需要对的数组进行扩容了,通常是扩容为之前数组的两倍。
对数组进行扩容需要重新计算key的下标,并且需要将键值对拷贝到新的数组,所以还是比较耗时的,尤其数据量大起来之后。