哈希表的一些原理分析

说来惭愧,本人在几年前就接触了数据结构,对哈希表的认识一直都比较模糊,在日常的学习工作中没少用到这一数据结构,比如像是python语言中的dict,或者是C++中的STL map,这段时间,我自己又重新学习了哈希表的一些设计原理,总结了一下,现在记录在这里,就当是帮助自己加深对hash的了解。

什么是哈希(Hash)表

Hash表也称散列表,也有直接译作哈希表,Hash表是一种特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区别,它能够快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。这个源于Hash表设计的特殊性,它采用了函数映射的思想将记录的存储位置与记录的关键字关联起来,从而能够很快速地进行查找。

哈希表的使用

在这里给出来一组数据,这组数据对应有两列(名字/号码),正常情况下,如果我们想要对这种类型的数据进行存储,像是int,string,char等数据类型是无法对这些数据进行存储的,

张三 13980593357
李四 15828662334
王五 13409821234
张帅 13890583472

在C语言中有一种数据类型叫做结构体(struct),我们首先考虑的是定义一个结构体,在结构体中对这两种数据进行定义,然后再将这些联系人的数据信息存储在一个链表中,通过遍历链表的方式,对每一个数据存储单元(struct)进行访问和读写。不过这样的一个劣势就是,时间复杂度为O(n),因为我们需要对链表进行从头到尾的遍历,即便是通过二分查找的方式进行访问查找,也只能够将时间复杂度降为O(log n).
那么有没有一种数据结构能够将查找的时间复杂度进一步降低呢?这里我们可以考虑使用哈希表(hash table)。
Hash表采用一个映射函数f :key -> address 将关键字映射到该记录在表中的存储位置,从而在想要查找该记录时,可以直接根据关键字和映射关系计算出该记录在表中的存储位置,通常情况下,这种映射关系称作为Hash函数,而通过Hash函数和关键字计算出来的存储位置(注意这里的存储位置只是表中的存储位置,并不是实际的物理地址)称作为Hash地址。比如上述例子中,假如联系人信息采用Hash表存储,则当想要找到“李四”的信息时,直接根据“李四”和Hash函数计算出Hash地址即可。下面讨论一下Hash表设计中的几个关键问题。

哈希函数的设计

Hash函数设计的好坏直接影响到对Hash表的操作效率。
例如对上述的联系人信息进行存储时,采用的Hash函数为:姓名的每个字的拼音开头大写字母的ASCII码之和。

address(张三)=ASCII(Z)+ASCII(S)=90+83=173;
    address(李四)=ASCII(L)+ASCII(S)=76+83=159;
    address(王五)=ASCII(W)+ASCII(W)=87+87=174;
    address(张帅)=ASCII(Z)+ASCII(S)=90+83=173;

假如只有这4个联系人信息需要进行存储,这个Hash函数设计的很糟糕。
首先,它浪费了大量的存储空间。因为假如采用char型数组存储联系人信息的话,每个人的信息需要12个字节来存储(手机号为11位,数值上为100多亿,2^64 =1.844674407371 * 10^19,2^32 = 4294967296,所以需要64位也就是8个字节来存储手机号。每个汉字占两个字节,两个汉字占四个字节。所以总共需要8 + 4 = 12Byte)这样的话,至少需要开辟174*12字节的空间。然而空间利用率只有4/174,不到3%。
另外,根据Hash函数计算结果之后,address(张三)和address(张帅)具有相同的地址,这种现象称作冲突,对于174个存储空间中只需要存储4条记录就发生了冲突,这样的Hash函数设计是很不合理的。所以在构造Hash函数时应尽量考虑关键字的分布特点来设计函数使得Hash地址随机均匀地分布在整个地址空间当中。
通常有以下几种构造Hash函数的方法:

直接定址法

取关键字或者关键字的某个线性函数作为Hash地址,即address(key) = a*key + b; 如果知道学生的学号是从2000开始,最大为4000,则可以将address(key) =key-2000作为Hash地址。

平方取中法

对关键字进行平方计算,然后取结果的中间几位作为Hash地址,假如有以下关键字序列{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取中间的两位数{72,89,00}作为Hash地址。

折叠法

将关键字拆分成几个部分,然后将这几个部分组合在一起,以特定的方式进行转化形成Hash地址。例如假如知道某图书的SBN号为:8903-241-23,可以将address(key)=89+03+24+12+3作为Hash地址。

除留取余法

如果知道Hash表的最大长度为m,可以取不大于m的最大质数p,然后对关键字进行取余运算,address(key)=key % p

在这里p的选取非常关键,p选择的好的话,能够最大程度地减少冲突,p一般取不大于m的最大质数。

Hash表大小的确定

Hash表大小的确定非常关键,如果Hash表的空间远远大于最后实际存储的记录个数,就会造成较大的空间浪费。如果选取小了的话,则容易造成冲突。在实际情况中,一般需要根据最终记录存储个数和关键字的分布特点来确定Hash表的大小。还有一种情况时可能事先不知道最终需要存储的记录个数,则需要动态维护Hash表的容量,此时可能需要重新计算Hash地址。

冲突的解决

如果产生了Hash冲突,就需要办法来解决,通常有如下两种方法:

开放定址法

即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中。比较常用的探测方法有线性探测法,比如有一组关键字 {12,13,25,23,38,34,6,84,91},Hash表长为14,Hash函数为address(key)=key%11 ,当插入12,13,25时可以直接插入,而当插入23时,地址1被占用了,因此沿着地址1依次往下探测(探测步长可以根据情况而定),直到探测到地址4,发现为空,则将23插入其中。

链地址法

采用数组和链表相结合的办法,将Hash地址相同的记录存储在一张线性表中,而每张表的表头的序号即为计算得到的Hash地址。如上述例子中,采用链地址法形成的Hash表存储表示为:


Hash链地址法演示

Hash表的平均查找长度

Hash表的平均查找长度包括查找成功时候的平均查找长度和查找失败时候的平均查找长度。
查找不成功时的平均查找长度相当于在表中查找元素不成功时的平均比较次数,可以理解为向表中插入某个元素,该元素在每个位置都有可能,然后计算出在每个位置能够插入时需要比较的次数,再除以表长即为查找不成功时的平均查找长度。

这里我们将关键字序列{7,8,30,11,18,9,14}散列存储到散列表中。散列表的存储空间是一个下标从0开始的一维数组,长度为10,即{0,1,2,3,4,5,6,7,8,9}.散列函数为:H(key) = (key * 3) % 7,处理冲突采用了线性探测再散列法。

Step 1 求散列表

H(7) = (7*3)%7 = 0
H(8) = (8*3)%7 = 0
H(30) = (30*3)%7 = 6
H(11) = (11*3)%7 = 5
H(18) = (18*3)%7 = 5
H(9) = (9*3)%7 = 6
H(14) = (14*3)%7 = 0

按关键字序列顺序依次向哈希表中填入,发生冲突后按照“线性探测”探测到第一个空位置填入。
H(7) = 0,key = 7应插在第0个位置,因为第0个位置为空,可以直接插入。
H(8) = 3,key = 8应插在第3个位置,因为第3个位置为空,可以直接插入。
H(30) = 6,key = 30应插在第6个位置,因为第6个位置为空,可以直接插入。
H(11) = 5,key = 11应插在第5个位置,因为第5个位置为空,可以直接插入。
H(18) = 5,key = 18应插在第5个位置,但是第5个位置已经被key=11占据了,所以往后挪一位到第6个位置,但是第6个位置被key=30占据了,再往后挪一位到第7个位置,这个位置是空的,所以key=18就插到这个位置
H(9) = 6,key = 9应插在第6个位置,但是第6个位置已经被key = 30占据,所以需要往后挪一位到第7个位置,但是第7个位置已经被key = 18占据,所以再往后挪移到第8个位置,这个位置是空的,所以key = 9就插到这个位置。
H(14) = 0,key = 14应插在第0个位置,但第0个位置已被key=7占据,所以往后挪移一位到第1个位置,这个位置是空的,所以key=14就插到这个位置。

最终的插入结果如下表所示:

address 0   1   2   3   4   5   6   7   8   9
key     7   14      8       11  30  18  9   

Step2 求查找成功的平均长度
查找7,H(7) = 0,在0的位置,一下子就找到了7,查找长度为1。
查找8,H(8) = 3,在3的位置,一下子就找到了8,查找长度为1。
查找30,H(30) = 6,在6的位置,一下子就找到了30,查找长度为1。
查找11,H(11) = 5,在5的位置,一下子就找到了11,查找长度为1。
查找18,H(18) = 5,第一次在5的位置没有找到18,第二次往后挪移一位到6的位置,仍没有找到,第三次再往后挪移一位到7的位置,找到了,查找长度为3。
查找9,H(9) = 6,第一次在6的位置没找到9,第二次往后挪移一位到7的位置,仍没有找到,第三次再往后挪移一位到8的位置,找到了,查找长度为3.
查找14,H(14) = 0,第一次在0的位置没找到14,第二次往后挪移一位到1的位置,找到了,查找长度为2。
所以,查找成功的平均查找长度为(1 + 1 + 1 + 1 + 3 + 3 + 2) / 7 = 12 / 7

Step3 求查找不成功的平均查找长度
查找不成功,说明要查找的数字肯定不在上述的散列表中。
因为这里哈希函数的模为7,所以要查找的数的初始地址只可能位于0~6的位置上。
地址0,到第一个关键字为空的地址2需要比较3次,因此查找不成功的次数为3。比如要查找的数为28,H(28) = (28 * 3) % 7 = 0。即28对应的地址是0,由于存放在0位置的数是7,所以往后挪移一位,发现在1位置存放的数是14,继续往后挪一位,发现位置2上没有数。至此就知道28不在这个哈希表里,即查找28失败。
地址1,到第一个关键字为空的地址2需要比较2次,因此查找不成功的次数为2。
地址2,到第一个关键字为空的地址2需要比较1次,因此查找不成功的次数为1。
地址3,到第一个关键字为空的地址4需要比较2次,因此查找不成功的次数为2。
地址4,到第一个关键字为空的地址4需要比较1次,因此查找不成功的次数为1。
地址5,到第一个关键字为空的地址9需要比较5次,因此查找不成功的次数为5。
比如要查找的数为4,H(4) = (4 * 3) % 7 = 5,所以从地址5开始查找,最终发现地址5、地址6、地址7、地址8上存放的数都不是5,并且地址9的位置上没放数据,至此可知5不在这个哈希表里。
地址6,到第一个关键字为空的地址9需要比较4次,因此查找不成功的次数为4。
所以,查找不成功的平均查找长度为(3 + 2 + 1 + 2 + 1 + 5 + 4)/ 7 = 18 / 7

作者:Paul 5/28/2019

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容

  • Hash表也叫散列表,是一张非常重要的数据结构,很多缓存技术的核心就是在内存中维护一张大的Hash表 简单回顾其他...
    Mr_Guo_Coding阅读 2,127评论 0 3
  • 哈希表定义 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结...
    n油炸小朋友阅读 4,863评论 0 22
  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,784评论 0 13
  • 散列表,它是基于快速存取的角度设计的,也是一种典型的“空间换时间”的做法。顾名思义,该数据结构可以理解为一个线性表...
    yeying12321阅读 3,691评论 0 6
  • 今天放学我是去小饭桌那里,然后我跟那里的老师说我考了99。我对这个成绩不满意。因为我错在把j的勾写错方向了。不过这...
    翟泽源阅读 216评论 0 0