散列表
- 我们可以总结出这样的规律:散列表来源于数组,它借助散列函数对数组这种数据结构进行扩展,散列表用的就是数组支持按照下标随机访问的时候,时间复杂度是 O(1) 的特性。我们通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当我们按照键值查询元素时,我们用同样的散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。
散列函数
-
构造散列函数的三点基本要求:
-
散列函数计算得到的散列值是一个非负整数;
因为数组下标是从 0 开始的,所以散列函数生成的散列值也要是非负整数。
-
如果 key1 = key2,那 hash(key1) == hash(key2);
相同的 key,经过散列函数得到的散列值也应该是相同的。
-
如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。
在真实的情况下,要想找到一个不同的 key 对应的散列值都不一样的散列函数,几乎是不可能的,即发生了散列冲突
-
-
散列冲突
不管采用哪种探测方法,当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,我们会尽可能保证散列表中有一定比例的空闲槽位。我们用装载因子(load factor)来表示空位的多少。
散列表的装载因子=填入表中的元素个数/散列表的长度
-
开放寻址法
开放寻址法的核心思想是,如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。
当数据量比较小、装载因子小的时候,适合采用开放寻址法。
-
线性探测:线性探测每次探测的步长是 1
当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
在散列表中查找元素的过程有点儿类似插入过程。我们通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素。如果相等,则说明就是我们要找的元素;否则就顺序往后依次查找。如果遍历到数组中的空闲位置,还没有找到,就说明要查找的元素并没有在散列表中。
-
二次探测:二次探测探测的步长就变成了原来的“二次方”
-
双重散列
所谓双重散列,意思就是不仅要使用一个散列函数。我们使用一组散列函数 hash1(key),hash2(key),hash3(key)……我们先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。
-
-
链表法
在散列表中,每个“桶(bucket)”或者“槽(slot)”会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中。
当插入的时候,我们只需要通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可,所以插入的时间复杂度是 O(1)。当查找、删除一个元素时,我们同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除。
链表法对内存的利用率比开放寻址法要高。
链表因为要存储指针,所以对于比较小的对象的存储,是比较消耗内存的,还有可能会让内存的消耗翻倍。而且,因为链表中的结点是零散分布在内存中的,不是连续的,所以对 CPU 缓存是不友好的,这方面对于执行效率也有一定的影响。
如果我们存储的是大对象,也就是说要存储的对象的大小远远大于一个指针的大小(4 个字节或者 8 个字节),那链表中指针的内存消耗在大对象面前就可以忽略了。
我们对链表法稍加改造,可以实现一个更加高效的散列表。那就是,我们将链表法中的链表改造为其他高效的动态数据结构,比如跳表、红黑树。这样,即便出现散列冲突,极端情况下,所有的数据都散列到同一个桶内,那最终退化成的散列表的查找时间也只不过是 O(logn)。这样也就有效避免了前面讲到的散列碰撞攻击。
基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。
-
哈希表 Hash Table
是一种存储记录的连续内存,通过哈希函数的应用,可以快速存取与查找数据。基本上,所谓哈希法就是将本身的键值,通过特定的数学函数运算或使用其他方法,转换成相对应的数据存储地址。
- bucket(桶):哈希表中存储数据的位置,每一个位置对应到唯一的一个地址(bucket address)
- slot(槽):每一个记录中可能包含好几个字段,而slot指的是“桶”中的字段
- collision(碰撞):两项不同的数据,经过哈希函数运算后,对应到相同的地址。
- 溢出:如果数据经过哈希函数运算后,所对应到的bucket已满,就会使bucket发生溢出。
- 哈希表:存储记录的连续内存。哈希表示一种类似数据表的索引表格,可分为n个bucket,每个bucket又可分为m个slot。
- 同义词:两个标识符I1和I2经哈希函数运算后所得的数值相同,即f(I1)=f(I2),就称I1与I2对于f这个哈希函数是同义词。
- 加载密度:指标识符的使用数量除以哈希表内槽的总数
- 完美哈希:没有碰撞也没有溢出的哈希函数
哈希函数设计原则
- 降低碰撞和溢出的产生
- 哈希函数不宜过于复杂,越容易计算越佳
- 尽量把文字的键值转换成数字的键值,以利于哈希函数的运算
- 所设计的哈希函数计算得到的值,尽量能均匀地分布在每一个桶中,不要太过于集中在某些桶内,这样就可以降低碰撞,并减少溢出的处理。