内核数据结构之认识哈希表

关于哈希表

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数(哈希函数),存放记录的数组叫做散列表。Hash的四大优点如下所示

  • 事先不需要排序。
  • 搜寻速度与数据多少无关。
  • 数字签名的密码技术保密性(Security)高。
  • 可做数据压缩(Data Compression),以节省空间。

Linux内核里的哈希表应用非常广泛。为什么哈希表能这么神通广大?哈希表能够实现高效的数据存储和查找,而存储和查找是编程中应用最广泛的两个操作。

hash函数

如何判定一个hash函数的好坏呢?hash的中文意思是“散列”,可解释为:分散排列。一个好的hash函数应该做到对所有元素平均分散排列,尽量避免或者降低他们之间的冲突(Collision)。hash函数的选择必须慎重,如果不幸所有的元素之间都产生了冲突,那么hash表将退化为链表,其性能会大打折扣,时间复杂度迅速降为O(n),绝对不要存在任何侥幸心理,因为那是相当危险的。历史上就出现过利用Linux内核hash函数的漏洞,成功构造出大量使hash表发生碰撞的元素,导致系统被DoS,所以目前内核的大部分hash函数都有一个随机数作为参数进行掺杂,以使其最后的值不能或者是不易被预测。这又对 hash函数提出了第二点安全方面的要求:hash函数最好是单向的,并且要用随机数进行掺杂。也许会想到单向散列函数md4和md5,但他们是不适合的,因为hash函数需要有相当好的性能。Linux内核里面用的jhash是一个久经考验,并被实践证明经得起考验的hash函数,可以CPMS(Copy Paste Modify Save)之。Jhash的作者Bob Jenkins在其网站上还公布了诸如针对能预知的数据进行hash的hash函数--完美(perfect)hash函数等一系列其他hash函数。bucket的英文解释:

Hash table lookup operations are often O(n/m) (where n is the number of objects in the table and m is the number of buckets), which is close to O(1), especially when the hash function has spread the hashed objects evenly through the hash table, and there are more hash buckets than objects to be stored.

翻译:哈希表查找操作通常是O(n/m)(其中n是表中的对象数,m是存储桶数),接近O(1),特别是当哈希函数已将哈希对象均匀地分布在哈希表中,并且哈希存储桶数多于要存储的对象时。

Hashtable 对象是由包含集合中元素的哈希桶(Bucket)所组成的。而Bucket是Hashtable内元素的虚拟子群组,可以让大部分集合中的搜寻和获取工作更容易、更快速。当对象加入至 Hashtable时,它存储在与对象哈希程序代码相符的哈希程序代码相关的Bucket中。当在Hashtable内搜寻值时,哈希程序代码会为该值产生,并且会搜寻与该哈希程序代码相关的Bucket。

Hashtable由多个Bucket组成,Bucket以HashKey值为索引,每个Bucket中存放着所有HashKey相同的(Key, Value)。

如图所示,BucketNum = 5, DataNum = 7, 可见 k = 1.4 有一些冲突,更能很好地看出拉链法是如何解决冲突问题的:

如Key=A Key=E Key=F 算出来的 HKey 均为1,所以(A, ValueA) (B, ValueB) (C, ValueC) 均放入HKey = 1 的 Bucket中;

常用的构造散列函数的方法

散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。散列表的常用构造方法有:
(1)直接定址法

(2)数字分析法

如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位,构成哈希地址。例如,有80个记录,关键字为8位十进制整数d1d2d3…d7d8,如哈希表长取100,则哈希表的地址空间为:00~99。假设经过分析,各关键字中 d4和d7的取值分布较均匀,则哈希函数为:h(key)=h(d1d2d3…d7d8)=d4d7。例如,h(81346532)=43,h(81301367)=06。相反,假设经过分析,各关键字中 d1和d8的取值分布极不均匀, d1 都等于5,d8 都等于2,此时,如果哈希函数为:h(key)=h(d1d2d3…d7d8)=d1d8,则所有关键字的地址码都是52,显然不可取。

(3)平方取中法

当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。 
例:我们把英文字母在字母表中的位置序号作为该英文字母的内部编码。例如K的内部编码为11,E的内部编码为05,Y的内部编码为25,A的内部编码为01, B的内部编码为02。由此组成关键字“KEYA”的内部代码为11052501,同理我们可以得到关键字“KYAB”、“AKEY”、“BKEY”的内部编码。之后对关键字进行平方运算后,取出第7到第9位作为该关键字哈希地址,如图所示。

关键字    内部编码    内部编码平方值    H(k)关键字的哈希地址
KEYA    11050201    122157778355001    778
KYAB    11250102    126564795010404    795
AKEY    01110525    001233265775625    265
BKEY    02110525    004454315775625    315

(4)分段叠加法

这种方法是按哈希表地址位数将关键字分成位数相等的几部分(最后一部分可以较短),然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。具体方法有折叠法移位法

  • 移位法是将分割后的每部分低位对齐相加
  • 折叠法是从一端向另一端沿分割界来回折叠(奇数段为正序,偶数段为倒序),然后将各段相加。

例如:key=12360324711202065,哈希表长度为1000,则应把关键字分成3位一段,在此舍去最低的两位65,分别进行移位叠加和折叠叠加,求得哈希地址为105和907,如图所示。

è¿éåå¾çæè¿°
(5)随机数法

采用一个伪随机函数做哈希函数,即h(key)=random(key)。

在实际应用中,应根据具体情况,灵活采用不同的方法,并用实际数据测试它的性能,以便做出正确判定。通常应考虑以下五个因素 :

  • 计算哈希函数所需时间 (简单)。
  • 关键字的长度。
  • 哈希表大小。
  • 关键字分布情况。
  • 记录查找频率

(6)除留余数法

假设哈希表长为m,p为小于等于m的最大素数,则哈希函数为h(k)=k % p ,其中%为模p取余运算。

例如,已知待散列元素为(18,75,60,43,54,90,46),表长m=10,p=7,则有

h(18)=18 % 7=4 h(75)=75 % 7=5 h(60)=60 % 7=4 
h(43)=43 % 7=1 h(54)=54 % 7=5 h(90)=90 % 7=6 
h(46)=46 % 7=4

此时冲突较多。为减少冲突,可取较大的m值和p值,如m=p=13,结果如下:

h(18)=18 % 13=5 h(75)=75 % 13=10 h(60)=60 % 13=8 
h(43)=43 % 13=4 h(54)=54 % 13=2 h(90)=90 % 13=12 
h(46)=46 % 13=7

此时没有冲突

0123456789101112
  54 4318 4660 75 90

处理冲突的方法

散列表函数设计好的情况下,可以减少冲突,但是无法完全避免冲突。常见有冲突处理方法有:
(1)开放定址法

这种方法也称再散列法,其基本思想是:

当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,**再以p为基础,产生另一个哈希地址**p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。

(2)再散列法

这种方法是同时构造多个不同的哈希函数:

Hi=RH1(key)  i=1,2,…,k

当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

(3)链地址法(拉链法)

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

(4)建立一个公共溢出区

这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

散列表查找性能分析

  散列表的查找过程基本上和造表过程相同。一些关键码可通过散列函数转换的地址直接找到,另一些关键码在散列函数得到的地址上产生了冲突,需要按处理冲突的方法进行查找。在介绍的三种处理冲突的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。所以,对散列表查找效率的量度,依然用平均查找长度来衡量。
  查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:
   1. 散列函数是否均匀;
   2. 处理冲突的方法;
   3. 散列表的装填因子。

散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度。
   α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越 小,填入表中的元素较少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是装填因子α的函数,只是不同处理冲突的方法有不同的函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值