程序中存储的数据都可以理解为文本,文本可以是数值、可打印字符、Unicode编码等。当我们在存储结构中查找某个关键字时,可以用线性查找、二分查找、树查找等方法,这些方法都是基于关键字比较的方法,逐个比较当前访问的元素是不是要查询的关键字。有一种可以不比较关键字即可实现查找的方法,这就是散列法(也称哈希)。
哈希表分为内哈希表和外哈希表两种,内哈希表的基本数据结构是数组,关键字为k的元素存储在数组中下标为H(k)的位置,H(k)为哈希函数,外哈希表的基本数据结构是指向链表的指针数组,关键字为k的元素存储在指针数组下标为H(k)的链表中。
哈希表中需要关注三个问题:哈希函数、冲突处理方法、查找长度。
1、哈希函数
由于关键字k的存储位置由哈希值H(k)决定,所以应该尽量使得不同的关键字能够存储在不同的位置,即关键字均匀地分散在表中不同地方,以减少冲突。
常用的哈希函数有:
(1)直接定址法
直接以关键字或关键字的简单线性函数作为哈希地址,一般适用于关键字是整型的数据,表长由关键字取值范围决定。
(2)数字分析法
关键字是比较大的整型数据,如8位十进制数组成的关键字,取其中随机性比较好的几位作为哈希地址,表长由取的位数决定。
(3)平方取中法
取关键字k的平方的中间几位作为哈希地址,一个数平方后的中间几位数和数的每一位都相关,由此使随机分布的关键字得到的哈希地址也是随机的,表长由取的位数决定。
(4)折叠法
如果关键字的位数很多,且每一位上数字分布大致均匀时,可以将关键字分割成位数相同的几部分,然后取这几部分的叠加和(舍去进位)作为哈希地址。表长由叠加和的取值范围决定。
(5)除留余数法(最简单、最常用的方法)
取关键字被某个质数p除后所得余数为哈希地址,即H(k)=k mod p,p<=表长。也可以在使用平方取中法和折叠法之后再取模。
(6)随机数法
选择一个随机数生成器,以关键字为随机种子,得到的随机数为哈希地址,即H(k)=random(k)。
2、冲突处理方法
(1)开放定址法
将哈希表中的所有地址都开放,探测当前哈希地址是否冲突,如果冲突,则根据某种探测方法探测下一个位置,直到找到不再冲突的地址。
Hi=(H(k)+di)modm
,i=1,2,…,k(k<=m-1),H(k)为哈希函数,m为哈希表长,di为增量序列。当
di
=1,2,…,m-1,称为线性探测,当
di=12,−12,22,−22,...,k2,−k2
(k<=m/2),称为二次探测,当
di
为伪随机数序列,称为伪随机探测。
使用线性探测容易产生“二次聚集”现象,在冲突位置的后续空白位置填入新关键字,使得原本哈希值为空白位置的不同关键字产生了冲突。
(2)再哈希法
产生冲突时,换一个哈希函数试试,如果还冲突,继续换一个不同的哈希函数,直到冲突不再发生,即 Hi=RHi(k) , i=1,2,…,k, RHi 均是不同的哈希函数。
(3)链地址法
将所有关键字为同义词的记录存储在同一线性链表中,这种哈希表也称为带有溢出表的内哈希表,由两部分组成,一部分是主表元素,另一部分或者为空,或者由一个链表组成溢出表,主表和溢出表中的元素有相同的哈希地址。
这种带有溢出表的内哈希表,其空间利用率不高,如果哈希函数值分布不均匀,将有较多的空桶占用存储空间。如果省略主表中数据占用的空间,只保留溢出表的指针,这种哈希表就成了外哈希表。
(4)建立公共溢出区
假设哈希函数的值域为[0, m-1],则设向量HashTable[0..m-1]为基本表,每个分量存放一个记录,另设立向量OverTable[0..v]为溢出表。所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都顺序填入溢出表。
3、查找长度
建哈希表时采用怎样的哈希函数和冲突处理方法,查找时也是一样的。给定关键字k值,根据建表时设定的哈希函数求得哈希地址,若表中此位置没有记录,则查找不成功;否则比较关键字,若和给定值相等,则查找成功;否则根据建表时设定的冲突处理方法找“下一地址”,直到哈希表中某个位置为“空”或者表中所填记录的关键字等于给定值为止。
平均查找长度=各元素的查找次数之和/表长,取决于三个因素:哈希函数、冲突处理方法、装填因子,其中装填因子=表中已填入的记录数/表长。好的哈希函数冲突次数少,平均查找长度自然就短,采用线性探测的冲突处理方法由于会有“二次聚集”现象,其平均查找长度一般高于链地址法,装填因子大表示哈希表装满的程度大,再填记录时,发生冲突的可能性也大。
平均查找长度分为查找成功时的平均查找长度和查找失败时的平均查找长度,可以证明,平均查找长度是装填因子的函数,而不是表长的函数,因此不管表长多大,我们总可以选择一个合适的装填因子来将平均查找长度限定在一个范围内。
值得注意的是,若要在非链地址法处理冲突的表中删除一个记录,不能将该位置设为empty,而是应该将该位置设为deleted,以免找不到在它之后填入的“同义词”的记录。
4、哈希的应用
(1)海量数据中查找top k的记录
思路:将海量数据中的逐条记录hash映射到不同的小文件中保存,由于关键字相同,哈希值必定相同,这样可以保证相同的记录只会在同一个小文件中出现,然后用hash_map对每个小文件中的记录进行出现次数统计,进行堆排序得到小文件中的top k,最后取出每个小文件的top k,归并排序得到整个海量数据中的top k。
(2)两个大文件中查找相同记录
思路:将两个大文件分别hash映射成多个小文件,假设a1、b1是hash映射得到的小文件,将a1和b1加载到内存,对a1中的记录建哈希表,对b1的记录逐个查询是否在哈希表中,如果在,则将其写入到一个相同记录文件c1,接着继续处理a2、b2也得到一个相同记录文件c2,将所有的ci文件合并,就得到了两个大文件中的相同记录。
(3)MD5文件校验
Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,将一个文件的所有内容作为关键字,得到一个64位的MD5值。MD5值用于验证下载的文件是否与原文件一致,也用于“数字指纹”(或称为“数字签名”),因为对原文件作了修改之后很难得到相同的MD5值。