zoukankan      html  css  js  c++  java
  • 哈希表原理

    哈希表是最常用的数据结构之一,对于其用法,大家都非常熟悉,这里详细探讨一下其原理。哈希表的底层实际上是基于数组来存储的,当插入键值对时,并不是直接插入该数组中,而是通过对键进行Hash运算得到Hash值,然后和数组容量取模,得到在数组中的位置后再插入。取值时,先对指定的键求Hash值,再和容量取模得到底层数组中对应的位置,如果指定的键值与存贮的键相匹配,则返回该键值对,如果不匹配,则表示哈希表中没有对应的键值对。这样做的好处是在查找、插入、删除等操作可以做到(O(1)),最坏的情况是(O(n)),当然这种是最极端的情况,极少遇到。
    image
    不管哪门语言,实现一个HashMap的过程均可分为三大步骤:

    • 实现一个Hash函数
    • 合理解决Hash冲突
    • 实现HashMap的操作方法

    Hash函数

    Hash函数非常重要,一个好的Hash函数不仅性能优越,而且还会让存储于底层数组中的值分配的更加均匀,减少冲突发生。之所以是减少冲突,是因为取Hash的过程,实际上是将输入键(定义域)映射到一个非常小的空间中,所以冲突是无法避免的,能做的只是减少Hash碰撞发生的概率。具体实现时,哈希函数算法可能不同,在Rust及很多语言的实现中,默认选择SipHash哈希算法。

    默认情况下,Rust的HashMap使用SipHash哈希算法,其旨在防止哈希表碰撞攻击,同时在各种工作负载上提供合理的性能。虽然 SipHash 在许多情况下表现出竞争优势,但其中一个比其它哈希算法要慢的情况是使用短键,例如整数。这就是为什么 Rust 程序员经常观察到 HashMap 表现不佳的原因。在这些情况下,经常推荐 FNV 哈希,但请注意,它不具备与 SipHash 相同的防碰撞性。

    影响Hash碰撞(冲突)发生的除了Hash函数本身意外,底层数组容量也是一个重要原因。很明显,极端情况下如果数组容量为1,哪必然发生碰撞,如果数组容量无限大,哪碰撞的概率非常之低。所以,哈希碰撞还取决于负载因子。负载因子是存储的键值对数目与数组容量的比值,比如数组容量100,当前存贮了90个键值对,负载因子为0.9。负载因子决定了哈希表什么时候扩容,如果负载因子的值太大,说明存储的键值对接近容量,增加碰撞的风险,如果值太小,则浪费空间。

    所以,既然冲突无法避免,就必须要有解决Hash冲突的机制方法。

    处理冲突的几种方法

    主要有四类处理冲突的方法:

    • 外部拉链法(常用)
    • 开放定址法(常用)
    • 公共溢出区(不常用)
    • 再Hash法(不常用)

    外部拉链法

    主要思想是基于数组和链表的组合来解决冲突,桶(Bucket)中不直接存储键值对,每个Bucket都链接一个链表,当发生冲突时,将冲突的键值对插入链表中。外部拉链法的有点在于方法简单,非同义词之间也不会产生聚集现象(相比于开放定址法),并且其空间结构是动态申请的,所以比较适合无法确定表长的情况:缺点是链表指针需要额外的空间,遇到碰撞拒绝服务时会退化为单链表。

    同义词:两个元素通过Hash函数得到了相同的索引地址,这两个元素就叫做同义词。

    下面是外部拉链法的两种实现方法,主要区别在于桶(Bucket)中是否存储数据。
    image

    image

    开放定址法

    主要思想是发生冲突时,直接去寻找下一个空的地址,只要底层的表足够大,就总能找到空的地址。这个寻找下一个地址的行为,叫做探测。 ({hash_{i}=(hash(key)+d_{i})\,{mod {\,}}m}, i=1,2...k\,(kleq m-1))`,其中(hash(key))为哈希函数,(m)为哈希表长,(d_{i})为增量序列,(i)为已发生冲突的次数。根据增量序列取法的不同有多种探测方法:

    • (d_{i}=1,2,3...(m-1))称为线性探测(Linear Probing);即 (d_{i}=i),或者为其他线性函数。相当于逐个探测存放地址的表,直到查找到一个空单元,把散列地址存放在该空单元。
    • (d_{i}=pm 1^{2},pm 2^{2},pm 3^{2}...pm k^{2} (kleq m/2))称为平方探测(Quadratic Probing)。相对线性探测,相当于发生冲突时探测间隔$ d_{i}=i^{2}$个单元的位置是否为空,如果为空,将地址存放进去。
    • (d_{i}=伪随机数序列),称为伪随机探测。

    下图为线性探测:

    image

    公共溢出区

    主要思想是建立一个独立的公共区,把冲突的键值对都放在其中。不常用,这里不再细述。

    再Hash法

    主要思想是有冲突时,换另外一个Hash函数来算Hash值。不常用,这里不再细述。

    实现哈希表的操作方法

    主要是:

    • insert
    • remove
    • get
    • contains_key
    • ......等等......

    其中最重要的是插入、查找、删除这三个操作。

    参考文档Hash table

    关注微信公众号,推送常用数据结构、TCP/IP、分布式、后端开发等技术分享,共同学习进步!

    ---恢复内容结束---

  • 相关阅读:
    三数之和
    罗马数字与整数
    Oracle 开启或关闭归档
    Oracle RMAN scripts to delete archivelog
    Oracle check TBS usage
    Oracle kill locked sessions
    场景9 深入RAC运行原理
    场景7 Data Guard
    场景4 Data Warehouse Management 数据仓库
    场景5 Performance Management
  • 原文地址:https://www.cnblogs.com/s-lisheng/p/11295481.html
Copyright © 2011-2022 走看看