博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
哈希表原理
阅读量:4701 次
发布时间:2019-06-09

本文共 2129 字,大约阅读时间需要 7 分钟。

哈希表是最常用的数据结构之一,对于其用法,大家都非常熟悉,这里详细探讨一下其原理。哈希表的底层实际上是基于数组来存储的,当插入键值对时,并不是直接插入该数组中,而是通过对键进行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})\,{\bmod {\,}}m}, i=1,2...k\,(k\leq 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} (k\leq m/2)\)称为平方探测(Quadratic Probing)。相对线性探测,相当于发生冲突时探测间隔$ d_{i}=i^{2}$个单元的位置是否为空,如果为空,将地址存放进去。
  • \(d_{i}=伪随机数序列\),称为伪随机探测。

下图为线性探测:

image

公共溢出区

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

再Hash法

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

实现哈希表的操作方法

主要是:

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

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

参考文档

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

16bd107edb7eed59?w=173&h=171&f=jpeg&s=31832

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

转载于:https://www.cnblogs.com/s-lisheng/p/11295481.html

你可能感兴趣的文章
redmine 一键安装
查看>>
021-异步注册登录(检测用户名)
查看>>
gitlab、github账户密码修改后,记得修改本地账户的git凭据
查看>>
2019年春季学期第二周作业
查看>>
深入浅出 Java 中的包装类
查看>>
SQL点点滴滴_修改数据库的兼容级别
查看>>
赋予ANDROID模拟器root权限2.2
查看>>
requests:json请求中中文乱码处理
查看>>
Error:“const char*”类型的实参与“wchar_t”类型的形参不兼容
查看>>
C# 时间格式转换
查看>>
iOS百度地图SDK集成详细步骤
查看>>
linux下cetos7无线网络设置办法
查看>>
7.24 面向对象
查看>>
C++ primer 第七章之 类的静态成员
查看>>
mac Xvim 语法高亮
查看>>
解决PHP生成校验码时“图像因其本身有错无法显示”的错误
查看>>
Android中的文件存储
查看>>
git 教程
查看>>
JDBC,Hibernate,MyBatis的优缺点
查看>>
关于struct函数以及重载
查看>>