数据结构:散列表

散列表
  • 我们可以总结出这样的规律:散列表来源于数组,它借助散列函数对数组这种数据结构进行扩展,散列表用的就是数组支持按照下标随机访问的时候,时间复杂度是 O(1) 的特性。我们通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当我们按照键值查询元素时,我们用同样的散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。
散列函数
  • 构造散列函数的三点基本要求:

    1. 散列函数计算得到的散列值是一个非负整数;

      因为数组下标是从 0 开始的,所以散列函数生成的散列值也要是非负整数。

    2. 如果 key1 = key2,那 hash(key1) == hash(key2);

      相同的 key,经过散列函数得到的散列值也应该是相同的。

    3. 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。

      在真实的情况下,要想找到一个不同的 key 对应的散列值都不一样的散列函数,几乎是不可能的,即发生了散列冲突

  • 散列冲突

    不管采用哪种探测方法,当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,我们会尽可能保证散列表中有一定比例的空闲槽位。我们用装载因子(load factor)来表示空位的多少。

    散列表的装载因子=填入表中的元素个数/散列表的长度

    1. 开放寻址法

      开放寻址法的核心思想是,如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。

      当数据量比较小、装载因子小的时候,适合采用开放寻址法。

      • 线性探测:线性探测每次探测的步长是 1

        当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。

        在散列表中查找元素的过程有点儿类似插入过程。我们通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素。如果相等,则说明就是我们要找的元素;否则就顺序往后依次查找。如果遍历到数组中的空闲位置,还没有找到,就说明要查找的元素并没有在散列表中。

      • 二次探测:二次探测探测的步长就变成了原来的“二次方”

      • 双重散列

        所谓双重散列,意思就是不仅要使用一个散列函数。我们使用一组散列函数 hash1(key),hash2(key),hash3(key)……我们先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。

    2. 链表法

      在散列表中,每个“桶(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这个哈希函数是同义词。
  • 加载密度:指标识符的使用数量除以哈希表内槽的总数
  • 完美哈希:没有碰撞也没有溢出的哈希函数
哈希函数设计原则
  1. 降低碰撞和溢出的产生
  2. 哈希函数不宜过于复杂,越容易计算越佳
  3. 尽量把文字的键值转换成数字的键值,以利于哈希函数的运算
  4. 所设计的哈希函数计算得到的值,尽量能均匀地分布在每一个桶中,不要太过于集中在某些桶内,这样就可以降低碰撞,并减少溢出的处理。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值