因为上周面试时多次被问到 JDK 中的 HashTable、HashMap等类的内部结构,然后会顺便聊到哈希表的方方面面,所以这里重新读了一遍《数据结构(C语言版)》这本书的哈希表那一节,下面算是阅读笔记吧!
什么是哈希表
在线性表、树等数据结构中,记录在结构中的相对位置是随机的,和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需要进行一系列和关键字的比较。这一类查找方法建立在“比较”的基础上,查找的效率依赖于查找过程中所进行的比较次数。
理想的情况是希望不经过任何比较,一次存取便能得到所查记录,那就必须在记录的存储位置和它的关键字之间建立一个确定的对应关系 f,使每个关键字和结构中一个唯一的存取位置相对应。在此,我们称这个对应关系 f 为哈希(Hash)函数,按这个思想建立的表为哈希表。
冲突
对不同的关键字可能得到同一哈希地址,即 key1 != key2,而 f(key1) == f(key2),这种现象称为冲突(collision)。具有相同函数值的关键字对该哈希函数来说称为同义词(synonym)。
哈希函数的构造方法
若对于关键字集合中的任一个关键字,经哈希函数映像到地址集合中任何一个地址的概率是相等的,则称此类哈希函数为均匀的(Uniform)哈希函数。
常用的哈希函数构造方法有:
- 直接定址法:取关键字或关键字的某个线性函数值为哈希地址,即 H(key) = key 或 H(key) = a * key + b,其中 a 和 b 为常数(这种哈希函数叫做自身函数)。
- 数字分析法:取关键字的若干数位组成哈希地址。
- 平方取中法:取关键字平方后的中间几位为哈希地址。
- 折叠发:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址,这方法称为折叠发(folding)。
- 除留余数法:取关键字被某个不大于哈希表表长 m 的数 p 除后所得余数为哈希地址。即 H(key) = key MOD p, p <= m。这是一种最简单,也最常用的构造哈希函数的方法。它不仅可以对关键字直接取模(MOD),也可在折叠、平方取中等运算之后取模。
- 随机数法:选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key),其中 random 为随机函数。
实际工作中需视不同的情况采用不同的哈希函数。通常,考虑的因素有:
- 计算哈希函数所需时间(包括硬件指令的因素);
- 关键字的长度;
- 哈希表的大小;
- 关键字的分布情况;
- 记录的查找频率;
处理冲突的方法
均匀的哈希函数可以减少冲突,但不能避免。因此,如何处理冲突是哈希建表不可缺少的另一方面。通常处理冲突的方法有下列几种:
- 开放地址法:H(i) = (H(key) + d(i)) MOD m, i = 1, 2, ..., k (k <= m - 1),这一方法又可以细分为:a) 线性探测再散列;b) 二次探测再散列;c) 伪随机探测再散列;
- 再哈希法:H(i) = RH(i)(key) i = 1, 2, ..., k。RH(i) 均是不同的哈希函数,即在同义词产生地址冲突时计算另一个哈希函数地址直到冲突不再发生。
- 链地址法:将所有关键字为同义词的记录存储在同一线性链表中。
- 建立一个公共溢出区:即除了基本的哈希表之外,再建立一个溢出表,用来存放冲突关键字。
值得注意的是,若要在非链地址处理冲突的哈希表中删除一个元素,则需在该记录的位置上填入一个特殊的符号,以免找不到在它之后填入的“同义词”记录。
哈希表的查找分析
在一般情况下,处理冲突方法相同的哈希表,其平均查找长度依赖于哈希表的装填因子。装填因子定义为:x = 表中填入的记录数n / 哈希表的长度m
具体分析较复杂,多为数学公式,这里只记录下结果吧:从以上分析可见,哈希表的平均查找长度是装填因子 x 的函数,而不是表中填入的记录数 n 或者哈希表的长度 m 的函数。