哈希桶 entry_哈希表算法原理

基本概念

哈希表(Hash Table)是一种根据关键字直接访问内存存储位置的数据结构。通过哈希表,数据元素的存放位置和数据元素的关键字之间建立起某种对应关系,建立这种对应关系的函数称为哈希函数。

3deb42778c4200ae9de66300c9c9bcf6.png

哈希函数构造方法

哈希表的构造方法是:假设要存储的数据元素个数为n,设置一个长度为m(m≥n)的连续存储单元,分别以每个数据元素的关键字 Ki(0<= i <=n-1) 为自变量,通过哈希函数 hash(Ki)Ki 映射为内存单元的某个地址 hash(ki),并将该数据元素存储在该内存单元中。

从数学的角度来看,哈希函数实际上是关键字到内存单元的映射,因此我们希望用哈希函数通过尽量简单的运算,使得通过哈希函数计算出的哈希地址尽量均匀地被映射到一系列的内存单元中。

构造哈希函数有三个要点:

第一,运算过程要尽量简单高效,以提高哈希表的插入和检索效率;

第二,哈希函数应该具有较好的散列性,以降低哈希冲突的概率;

第三,哈希函数应具有较大的压缩性,以节省内存。

一般有以下几种常见的方法:

1)直接定址法

该方法是取关键字的某个线性函数值为哈希地址。可以简单的表示为:

6636ffcac5069cff097722973aa6031f.png

优点是不会产生冲突,但缺点是空间复杂度可能会很高,适用于元素较少的情况下;

2)除留余数法

它是用数据元素关键字除以某个常数所得的余数作为哈希地址,该方法计算简单,适用范围广,是最经常使用的一种哈希函数,可以表示为:

9c72c4f5da5299904091a6ae3509a783.png

该方法的关键是常数的选取,一般要求是接近或等于哈希表本身的长度,理论研究表明,该常数取素数时效果最好。

3)数字分析法

该方法是取数据元素关键字中某些取值较均匀的数字位来作为哈希地址的方法,这样可以尽量避免冲突,但是该方法只适合于所有关键字已知的情况。对于想要设计出更加通用的哈希表并不适用。

哈希冲突解决办法

在构造哈希表时,存在这样的问题,对于两个不同的关键字,通过我们的哈希函数计算哈希地址时却得到了相同的哈希地址,我们将这种现象称为哈希冲突(如图):

f334f3e7f73d28ec6fc75b9681ec5b19.png

哈希冲突主要与两个因素相关:

第一,填装因子,所谓的填装因子是指哈希表中已存入的数据元素个数与哈希地址空间大小的比值,即α=n/m,α越小,冲突的可能性就越小,相反则冲突可能性越大;但是α越小,哈希表的存储空间利用率也就很低,α越大,存储空间的利用率也就越高,为了兼顾哈希冲突和存储空间利用率,通常将α控制在0.6-0.9之间(JDK中取0.75),而.NET中的Hashtable则直接将α的最大值定义为0.72(注:虽然微软官方MSDN中声明Hashtable默认填装因子为1.0,事实上所有的填装因子都为0.72的倍数);

第二,与所用的哈希函数有关,如果哈希函数选择得当,就可以使哈希地址尽可能的均匀分布在哈希地址空间上,从而减少冲突的产生,但一个良好的哈希函数的得来很大程度上取决于大量的实践,不过幸好前人已经总结实践了很多高效的哈希函数,可以参考园子里大牛Lucifer的文章:数据结构 : Hash Table [I]

哈希冲突通常是很难避免的,解决哈希冲突有很多种方法,通常分为两大类:

1)开放定址法

它是一类以发生哈希冲突的哈希地址为自变量,通过某种哈希函数得到一个新的空闲内存单元地址的方法(如图),开放定址法的哈希冲突函数通常是一组;

e6e6fe80b589afd3de493112488f08d6.png

2)链表法

当未发生冲突时,则直接存放该数据元素;当冲突产生时,把产生冲突的数据元素另外存放在单链表中。

93440825d230ed43d349b90de8d3e511.png

Hashtable 和 Dictionary

.NET中实现了哈希表的类分别是Hashtable和Dictionary<TKey, TValue>,Hashtable由包含集合元素的存储桶组成,存储桶是Hashtable中各元素的虚拟子组,与大多数集合中进行的搜索相比,存储桶可使搜索更为便捷。Dictionary则是泛型版本的哈希表,与Hashtable的功能相同,对于值类型,特定类型(不包括Object)的性能优先于Hashtable,这是因为Hashtable的元素属于Object类型,所以在存储或者检索类型时通常发生装箱和拆箱操作;除此之外,虽然微软宣称Hashtable是线程安全的,可以允许多个读线程或一个写线程访问,但是事实是它也并非线程安全,在.NET Framework 2.0新引入的Dictionary仍旧为解决这个问题,其中限于公共静态方法是线程安全的,因此可以说Dictionary是非线程安全的,而且对整个集合的枚举过程对二者而言都不是线程安全的,因为当出现枚举与写访问互相争用这种情况发生时,则必须在整个枚举过程中对整个集合加锁。如果我们在使用.NET Framework 4.0以上版本,我们可以使用线程安全的ConcurrentDictionary;另一个比较重要的区别在于,虽然它们都实现了哈希表,但是二者却使用了完全不同的哈希冲突解决方法,Hashtable解决冲突的方式是开放定址法,而Dictionary则采用了链表法。

Hashtable的实现原理

Hashtable类中哈希函数的定义可以用如下递推公式来表示:

99a0c8cfe9167b68ca0be39a3bd5a682.png

c2170cb68b0c5494ad33ae192f4f93d1.png

通过简单的数学推导就可以得出其通项式公式即Hashtable的哈希函数簇为:

fd9827dee18488994f9bbac12baba991.png

因此我们就拥有了一系列的哈希函数:

768735530487dad7b3f37c9cf818cc95.png

,当我们向哈希表中增加元素时,则依次尝试使用这些哈希函数,直到找到相应的空闲内存单元地址为止,这种方式称为二度哈希。

在Hashtable类中,包含元素的存储桶被定义在结构体bucket中:

private struct bucket{public object key;public object val;public int hash_coll;}

其中前两个字段很容易理解,分别代表了哈希表中的关键字和值,对于第三个字段hash_coll,实际上保存了两种信息:关键字的哈希码和是否冲突,coll为collision(冲突)的缩写,该字段为32位整型类型,最高位为符号位,当最高位为0时,表示该数为正数,表示未发生冲突,为1时为负数,表示发生了冲突,剩下的其他位则用于保存哈希码。

下面我们来看一个简单的哈希表元素增删过程,使得我们对于哈希表的具体工作方式有一个更直观的了解,当我们未指定具体Hashtable容量大小时,来进行一组数据的插入操作,此时Hashtable类会自动初始化其容量为默认最小值3。

插入元素[20, “elem1”],根据Hashtable类哈希函数通项式,所以其哈希代码的值为

9cacfa2e8d714c8d5d7c6b7635b40a0a.png

,此时为第一次插入数据,因此不存在冲突,直接寻址到bucket[2],由于不存在冲突,因此hash_coll的值即为其key的哈希代码,存储结构如下图:

7e7512d991659c73e3cfc2505f9f9e6f.png

插入元素[33, “elem2”],同理

da2270761f660a529244f6f4dd55384d.png

,此时仍然不存在冲突,存储结构如下:

0f0053c89ee0e31fcbb4552fad357967.png

插入元素[40, “elem3”],此时的哈希表进行扩容,为什么会在此时扩容呢,哈希表的填装因子为2/3=0.66并未超过0.72,在.NET中,微软对填装因子进行了换算,通过填装因子与哈希表大小的乘积取整获得哈希表的最佳填装量即:3×0.72=2。扩容后的哈希表大小为原表容量大小的2倍后的质数,在本例中再次扩容后哈希表大小为7。进行扩容之后,原哈希表的已经存储的元素必须按照新的哈希表的哈希函数(其实哈希函数本身没有发生变动,发生变动的是哈希表的长度)进行计算,重新寻址,扩容后的哈希表如下:

7252020c57a5fcf721b78c8ce5acf807.png

完成扩容过程后才会对[40, “elem3”]进行插入操作,

50f957a622940a6fb4dac8423b6ea38d.png

,现在我们发现冲突产生了,因为此时bucket[5]的位置已经有元素了,此时进行二度哈希:

163a043dc36d153dde652175a2394970.png

ec7198fe79e2c614c6657f054a0b9eaf.png


此时哈希表中位置为1的空间仍旧处于空闲状态,因此进行插入操作,在将元素插入之前,由于bucket[5]出现了冲突,因此需要对其进行标记,将hash_coll的最高位置为1,表示其出现了冲突,所以完成插入后哈希表结构如下图:

b95d0e704cf206a49ec14793b8b4c29f.png

插入元素[55, “elem4”],同理,

fd6e6a9e63622e2607f9b7f0ec329202.png

,产生冲突,进行二度哈希:

a429ef813582397b5aaeb05583bd291f.png

5313e7af76d2476105c5c63773260957.png

完成插入后哈希表的存储结构为:

6be36f237ce1cc86842659dd94488166.png

删除元素[20, “elem1”],在删除元素时,同样需要根据哈希函数来进行寻址,如果有冲突,则进行二度哈希,但是值得注意的一点是,删除冲突标记元素(即元素的hash_coll值为负数)和非冲突标记元素是有差别的,在删除非冲突标记元素时,则直接将要删除的元素的键和值修改为null并将hash_coll置0即可,但是在删除冲突标记元素时,需将hash_coll的hash部分(即0-30位)置0以及将元素的值置为null,还需将该元素的键指向整个哈希表,之所以这样做是因为当索引为0的元素也出现冲突时,将无法判断该位置是一个空位还是非空位,那么再次进行插入时很可能将索引为0处的元素覆盖。删除[20, “elem1”]后的结构为:

668fd5b0ad2e57ea9104583bfd2fed96.png

Dictionary的实现原理

从.NET Framework 2.0开始,随着泛型的引入,类库提供一个新的命名空间System.Collection.Generic,并且在该命名空间下新增了Dictionary等泛型类。

Dictionary的哈希函数就相对简单,就是简单的除留余数法,对于冲突解决,Dictionary则采用了链表法,但是此时buckets数组已经退化为专门用于存储元素的位置(下标)的整型数组,包含元素的数据结构被定义为结构体Entry,通过一个Entry类型的数组entries专门用于存储元素,Entry的定义如下:

private struct Entry{public int hashCode;public int next;public TKey key;public TValue value;}

其中的next字段表示数组链表的下一个元素的下标,一个关于数据存储结构的简单图示如下:

我们用同样的方式来看看Dictionary插入和删除元素的简单过程:

插入元素[20, “elem1”],跟Hashtable类似,Dictionary初始化容量也为3(如果未指定初始化容量),Dictionary的哈希函数就非常简单了,除留余数法直接获取其哈希地址,

a635bbae3cd029044d9e07a0962c04d2.png

,那么此时在entries[0]直接保存下元素的键值以及哈希码,并将此时元素在entries数组中的索引赋值给buckets[2],如下图:

ca0ed119af1f2434447755c9d947dbb7.png

插入元素[33, “elem2”],其哈希地址为:

b61fa8489964d6cdbcade4704d86c623.png

,插入后存储结构如下:

47596a698973377c316e3e963de7a9bc.png

插入元素[40, “elem3”],计算后的哈希地址为

117c82a81c9647774dd5d3da267d97b4.png

,刚好未发生冲突,由于不受填装因子(此时填装因子为1)的约束,此时无须扩容,插入该元素后的存储结构为:

1558a000f782fdb72c54409587b5a95f.png

插入元素[55, “elem4”],此时Dictionary的容量已满,必须进行扩容操作,Dictionary的扩容和Hashtable的扩容策略一致,扩容后的Dictionary的容量大小为原Dictionary容量大小2倍后的质数即也为7,然后根据扩容后的Dictionary重新寻址,这意味着部分数据可能会引起冲突从而导致已有的链表会被打乱重新组织;Dictionary首先会将扩容前Dictionary中的entries中的元素全部复制到新的entries中,紧接着进行重新寻址,对于第一个元素[20, “elem1”],新的哈希地址为:

,于是buckets[6]的值被修改为0(即元素[20, “elem1”]在entries中的索引),同理对于33:

566e4faff4ae2e5a0039dd5cf1651b4f.png

,所以,buckets[5]=1,最后处理40,

71490fc795855d83a98c390dee92e7f0.png

,此时发生冲突,在通过链表法处理冲突时,Dictionary首先将新元素的next指向冲突位置的元素索引buckets[5],然后再将buckets[5]指向新的元素,此时一条只有两个元素的基于数组的链表形成,因此扩容之后的存储结构如下图:

82a1d6ec43ff4405c8b2edc04017fb63.png

在这里可以看出无论是Dictionary还是Hashtable,扩容带来的性能损耗代价都是相当昂贵的,因此我们最好能够预估出哈希表中最后容纳元素的总数,在哈希表初始化时就对其进行内存分配,从而避免不必要的扩容带来的性能损耗;

此时再插入元素[55, “elem4”],计算其哈希地址:

d0b1bd8e8ae251b71008ac71ae22860e.png

,再次发生冲突,那么按照刚刚的冲突解决办法,插入该元素之后的存储结构为:

c4d88bfedc4625e72e06204a90ab8a4f.png

最后插入元素[13, “elem5”],

d2ca0d7f3a632b1b5abbbd9a188a01b3.png

,再次冲突发生,那么插入该元素之后的结构图如下:

9fa3f22176571459acf73028e841c4bb.png

删除元素对于Dictionary来说就很简单了,如果在非冲突链上删除元素,非常简单,通过哈希算法寻址找到对应的元素删除并将buckets中对应的元素值修改为-1,如果在冲突链上删除元素,那么就是一个简单的删除链表元素的操作,在这里就留给读者去思考。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第一章 基础知识 8 1.1 对称算法 8 1.2 摘要算法 9 1.3 公钥算法 9 1.4 回调函数 11 第二章 openssl简介 13 2.1 openssl简介 13 2.2 openssl安装 13 2.2.1 linux下的安装 13 2.2.2 windows编译与安装 14 2.3 openssl源代码 14 2.4 openssl学习方法 16 第三章 堆栈 17 3.1 openssl堆栈 17 3.2 数据结构 17 3.3 源码 18 3.4 定义用户自己的堆栈函数 18 3.5 编程示例 19 第四章 哈希表 21 4.1 哈希表 21 4.2 哈希表数据结构 21 4.3 函数说明 23 4.4 编程示例 25 第五章 内存分配 27 5.1 openssl内存分配 27 5.2 内存数据结构 27 5.3 主要函数 28 5.4 编程示例 29 第六章 动态模块加载 30 6.1 动态库加载 30 6.2 DSO概述 30 6.3 数据结构 31 6.4 编程示例 32 第七章 抽象IO 34 7.1 openssl抽象IO 34 7.2 数据结构 34 7.3 BIO 函数 36 7.4 编程示例 36 7.4.1 mem bio 36 7.4.2 file bio 37 7.4.3 socket bio 38 7.4.4 md BIO 39 7.4.5 cipher BIO 40 7.4.6 ssl BIO 41 7.4.7 其他示例 42 第八章 配置文件 43 8.1 概述 43 8.2 openssl配置文件读取 43 8.3 主要函数 44 8.4 编程示例 44 第九章 随机数 46 9.1 随机数 46 9.2 openssl随机数数据结构与源码 46 9.3 主要函数 48 9.4 编程示例 48 第十章 文本数据库 50 10.1 概述 50 10.2 数据结构 51 10.3 函数说明 51 10.4 编程示例 52 第十一章 大数 54 11.1 介绍 54 11.2 openssl大数表示 54 11.3 大数函数 55 11.4 使用示例 58 第十二章 BASE64编解码 64 12.1 BASE64编码介绍 64 12.2 BASE64编解码原理 64 12.3 主要函数 65 12.4 编程示例 66 第十三章 ASN1库 68 13.1 ASN1简介 68 13.2 DER编码 70 13.3 ASN1基本类型示例 70 13.4 openssl 的ASN.1库 73 13.5 用openssl的ASN.1库DER编解码 74 13.6 Openssl的ASN.1宏 74 13.7 ASN1常用函数 75 13.8 属性证书编码 89 第十四章 错误处理 93 14.1 概述 93 14.2 数据结构 93 14.3 主要函数 95 14.4 编程示例 97 第十五章 摘要与HMAC 100 15.1 概述 100 15.2 openssl摘要实现 100 15.3 函数说明 101 15.4 编程示例 101 15.5 HMAC 103 第十六章 数据压缩 104 16.1 简介 104 16.2 数据结构 104 16.3 函数说明 105 16.4 openssl中压缩算法协商 106 16.5 编程示例 106 第十七章 RSA 107 17.1 RSA介绍 107 17.2 openssl的RSA实现 107 17.3 RSA签名与验证过程 108 17.4 数据结构 109 17.4.1 RSA_METHOD 109 17.4.2 RSA 110 17.5 主要函数 110 17.6编程示例 112 17.6.1密钥生成 112 17.6.2 RSA加解密运算 113 17.6.3签名与验证 116 第十八章 DSA 119 18.1 DSA简介 119 18.2 openssl的DSA实现 120 18.3 DSA数据结构 120 18.4 主要函数 121 18.5 编程示例 122 18.5.1密钥生成 122 18.5.2签名与验证 124 第十九章DH 126 19.1 DH算法介绍 126 19.2 openssl的DH实现 127 19.3数据结构 127 19.4 主要函数 128 19.5 编程示例 129 第二十章 椭圆曲线 132 20.1 ECC介绍 132 20.2 openssl的ECC实现 133 20.3 主要函数 135 20.3.1参数设置 135 20.3.2参数获取 136 20.3.3转化函数 137 20.3.4其他函数 137 20.4 编程示例 139 第二十一章 EVP 143 21.1 EVP简介 143 21.2 数据结构 143 21.2.1 EVP_PKEY 144 21.2.2 EVP_MD 144 21.2.3 EVP_CIPHER 145 21.2.4 EVP_CIPHER_CTX 146 21.3 源码结构 147 21.4 摘要函数 147 21.5 对称加解密函数 148 21.6 非对称函数 149 21.7 BASE64编解码函数 149 21.8其他函数 150 21.9 对称加密过程 152 21.10 编程示例 152 第二十二章 PEM格式 159 22.1 PEM概述 159 22.2 openssl的PEM实现 160 22.3 PEM函数 161 22.4 编程示例 161 第二十三章 Engine 165 23.1 Engine概述 165 23.2 Engine支持的原理 165 23.3 Engine数据结构 166 23.4 openssl 的Engine源码 167 23.5 Engine函数 167 23.6 实现Engine示例 169 第二十四章 通用数据结构 182 24.1通用数据结构 182 24.2 X509_ALGOR 182 24.3 X509_VAL 184 24.4 X509_SIG 185 24.5 X509_NAME_ENTRY 186 24.6 X509_NAME 187 24.7 X509_EXTENSION 193 24.8 X509_ATTRIBUTE 199 24.9 GENERAL_NAME 200 第二十五章 证书申请 203 25.1 证书申请介绍 203 25.2 数据结构 203 25.3 主要函数 204 25.4 编程示例 206 25.4.1生成证书请求文件 206 25.4.2 解码证书请求文件 208 第二十六章 X509数字证书 210 26.1 X509数字证书 210 26.2 opessl实现 210 26.3 X509数据结构 210 26.4 X509_TRUST与X509_CERT_AUX 214 26.5 X509_PURPOSE 215 26.6 主要函数 218 26.7 证书验证 221 26.7.1证书验证项 221 26.7.2 Openssl中的证书验证 221 第二十七章 OCSP 222 27.1 概述 222 27.2 openssl实现 222 27.3 主要函数 222 27.4编程示例 227 第二十八章 CRL 228 28.1 CRL介绍 228 28.2 数据结构 228 28.3 CRL函数 230 28.4 编程示例 231 第二十九章 PKCS7 233 29.1概述 233 29.2 数据结构 233 29.3 函数 234 29.4 消息编解码 235 29.4.1 data 235 29.4.2 signed data 236 29.4.3 enveloped 237 29.4.4 signed_and_enveloped 238 29.4.5 digest 238 29.4.6 encrypted 239 29.4.7 读取PEM 239 29.4.8 解码pkcs7 240 第三十章 PKCS12 241 30.1 概述 241 30.2 openss实现 241 30.3数据结构 242 30.4函数 243 30.5 编程示例 245 第三十一章 SSL实现 254 31.1概述 254 31.2 openssl实现 254 31.3 建立SSL测试环境 254 31.4 数据结构 256 31.5 加密套件 256 31.6 密钥信息 257 31.7 SESSION 258 31.8 多线程支持 258 31.9 编程示例 259 31.10 函数 270 第三十二章 Openssl命令 272 32.1概述 272 32.2 asn1parse 272 32.3 dgst 274 32.4 gendh 275 32.5 passwd 276 32.6 rand 276 32.7 genrsa 277 32.8 req 278 32.9 x509 280 32.10 version 283 32.11 speed 283 32.12 sess_id 284 32.13 s_server 284 32.14 s_client 286 32.15 rsa 288 32.16 pkcs7 289 32.17 dsaparam 290 32.18 gendsa 291 32.19 enc 291 32.20 ciphers 292 32.21 CA 293 32.22 verify 296 32.23 rsatul 297 32.24 crl 299 32.25 crl2pkcs7 300 32.26 errstr 300 32.27 ocsp 301 32.28 pkcs12 304 32.29 pkcs8 306 32.30 s_time 307 32.31 dhparam和dh 308 32.32 ecparam 309 32.33 ec 310 32.34 dsa 311 32.35 nseq 312 32.36 prime 313 32.37 smime 313
HashMap是一种散列表,它存储的内容是键值对(key-value)映射。当我们将一个键值对存储在HashMap中时,它会根据键的哈希码找到对应的(bucket),然后将键值对存储在中。当我们需要获取一个键对应的值时,HashMap会根据键的哈希码找到对应的,然后在中查找对应的值。 在面试中,如果被问到HashMap是否有序,我们需要回答说HashMap不是有序的,因为HashMap中的键值对是根据哈希码存储的,而哈希码是无序的。如果被问到HashMap的存储原理,我们需要回答说HashMap是通过哈希算法将键映射到中的,然后将键值对存储在中。如果被问到哈希算法的底层实现,我们需要回答说哈希算法的底层实现是哈希表。 以下是一个简单的HashMap的实现代码,仅供参考: ```java public class MyHashMap<K, V> { private static final int DEFAULT_CAPACITY = 16; private static final float DEFAULT_LOAD_FACTOR = 0.75f; private Entry<K, V>[] table; private int size; private int capacity; private float loadFactor; public MyHashMap() { this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); } public MyHashMap(int capacity, float loadFactor) { this.capacity = capacity; this.loadFactor = loadFactor; this.table = new Entry[capacity]; } public void put(K key, V value) { int index = hash(key); Entry<K, V> entry = table[index]; while (entry != null) { if (entry.key.equals(key)) { entry.value = value; return; } entry = entry.next; } Entry<K, V> newEntry = new Entry<>(key, value); newEntry.next = table[index]; table[index] = newEntry; size++; if (size > capacity * loadFactor) { resize(); } } public V get(K key) { int index = hash(key); Entry<K, V> entry = table[index]; while (entry != null) { if (entry.key.equals(key)) { return entry.value; } entry = entry.next; } return null; } private int hash(K key) { return key.hashCode() % capacity; } private void resize() { capacity *= 2; Entry<K, V>[] newTable = new Entry[capacity]; for (Entry<K, V> entry : table) { while (entry != null) { Entry<K, V> next = entry.next; int index = hash(entry.key); entry.next = newTable[index]; newTable[index] = entry; entry = next; } } table = newTable; } private static class Entry<K, V> { K key; V value; Entry<K, V> next; public Entry(K key, V value) { this.key = key; this.value = value; } } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值