概述
什么是散列表? 如果说起它的另一个名字, 你一定很熟悉, 它的英文叫"Hash Table", 哈希表, 很熟悉吧.
散列的思想, 其实就是利用数组的随机访问特性, 将key-value形式的数据, 其中的key转换成数组下标, 即可实现将其存放到数组中, 进而实现随机访问.
而其中将key转换成数字的函数, 被称为散列函数, 或哈希函数.
为了方便大家看, 以下统一称为哈希, 知道这俩是一回事就行.
哈希函数
设计一个哈希函数, 有如下三点要求:
-
散列函数计算得出的值是一个正整数(数组下标嘛)
-
若key相等, 则计算后的哈希值相等
-
若key不相等, 则计算后的哈希值不相等
后面两点, 说白了就是, 计算后的哈希值是唯一的, 不变的.
要想达到第二点要求, 应该不难, 只要算法固定, 输入与输出就应该是固定的嘛
但是, 要想实现第三点, 就不一样了, 要想找到一个完全符合的, 几乎是不可能. 而且, 要想将其放到数组中, 数组的大小是有限的啊, 所以, 当出现计算后两个哈希值相等的情况, 就是哈希冲突.
哈希冲突
既然没有完美的哈希函数, 那么就不可避免会发生哈希冲突, 那么就要解决哈希冲突, 常用的有开放寻址法和链表法.
1. 开放寻址法
开放寻址法的思想很简单, 当发生哈希冲突的情况时, 就从当前位置往后找, 找到第一个空缺的位置放入.
对于开放寻址法, 查找操作也顺理成章, 计算key的哈希值后, 查看其下标元素是否为要寻找的元素, 若不是, 向后寻找, 一直找到出现空位, 则说明key不在表中.
但是, 删除操作就比较麻烦了, 因为查找是通过空位来判断的, 若直接删除key, 就会在下次查找时出现空位而打断本来应该继续的查找. 对于这种情况, 我们可以将删除的空位标记为delete, 查找时遇到delete不会中断就好了.
上面说的这种查找方法叫线性探测法, 顾名思义, 就是一个一个往后找, 另外还有两种经典查找方法: 二次探测和双重散列.
二次探测: 线性探测每次探测的步长(每次往后找的个数)是1, 也就是说探测的下标是k+0,k+1,k+2,k+3. 二次探测就是每次访问的步长变为原来的二次, 探测的下标为: k+0,k+1,k+2,k+9
双重散列: 就是不单单使用一个哈希函数, 而是使用一串, 当第一个哈希函数发生冲突时, 就用第二个计算, 再冲突, 再用第三个计算, 直到找到空位
问题
很明显, 开放寻址法有很大的问题. 当表中数据越来越多的时候, 哈希冲突的概率也会越来越大, 对应的查找操作也就会越来越慢, 甚至最终会遍历整个表.
装载因子
用装载因子来表示哈希表中空位的多少, 其计算公式是:
装载因子=表中元素个数 / 表的长度
装载因子越大, 说明空位越少, 冲突越多, 哈希表的性能越低.
2. 链表法
使用链表法来解决哈希冲突相对来说更为常见一些, Java中的HashMap就是这么处理的.
通过一张图来简单说明链表的处理方法:
当发生哈希冲突时, 将数据插入到对应的链表中. Java中的HashMap就是通过hashcode方法计算数组下标, 再通过equals方法判断两对象是否相等.