Q: 如何快速地存取员工的信息?
A: 假设现在要写一个程序, 存取一个公司的员工记录, 这个小公司大约有 1000 个员工, 每个员工记录需要 1024 个字节的存储空间, 因此整个数据库的大小约为 1MB 一般的计算机内存都可以满足
为了尽可能地存取每个员工的记录, 使用工号从 1(公司创业者)到 1000(最近雇佣的工人)将工号作为关键字 (事实上, 用其他作为关键字完全没有必要) 即使员工离职不在公司, 他们的记录也是要保存在数据库中以供参考, 在这种情况下需要使用什么数据结构呢?
A: 一种可能使用数组, 每个员工占数组里的一个元素数组下标是当前记录的员工工号, 数组的类型如下图:
如果知道数组下标, 要访问特定的数组数据项非常方便 HR 要查找 Herman Alcazar, 她知道 Alcazar 的工号是 72, 所以她键入这个数字, 程序直接检索数组下标为 72 的元素, 总共只需一条语句:
EmpRecord record = employees[72];
增加一个新记录也非常快, 只需要把它插入到最后一个元素的后面下一个记录将被放在第 1001 个元素, 插入新记录也只需要一条语句:
employees[total++] = newRecord;
备注: 数组大概需要比当前员工的总数要大一些, 为扩展留出的空间, 但不能指望可以大量扩展数组容量
Q: 如何快速地查找字典里的单词?
A: 如果想要把一本英文字典的每个单词, 从 a 到 zyzzyva, 都写入到内存, 以便快速读写, 那么哈希表是一个不错的选择
A: 例如, 想在内存中存储 50000 个英文单词, 起初可能考虑到每个单词占据一个数组元素, 那么数组的大小是 50000 个, 同时可以使用数组下标存取单词, 那么数组下标和单词有什么关系呢? 例如给出一个单词 morphosis, 怎么能找到它的数组下标呢?
A: ASCII 码从 0 到 255, 可以容纳字母标点等字符假设我们这个字典不使用大写字母, 为了省下更多的内存, 我们可以设计一种自己的编码方案, 其中 a 是 1,b 是 2,c 是 3, 以此类推, 直到 26 代表 z, 还要把空格用 0 代表, 所以总共有 27 个字符(这里假设这个字典不使用大写字母)
Q: 如何把代表单个字母的数字组合成代表整个单词的数字呢?
A: 下面介绍两种具有代表性的方法, 以及它们的优点和缺点
A: 方法一: 把单词里每个字符的编码求和
例如把单词 cats 转化为数字, 用前面创造的编码方案转换单词如下:
- c=3
- a=1
- t=20
- s=19
因此 3+1+20+19=43, 那么在字典里 cats 单词存储在下标为 43 的数组里所有的英文单词都可以使用这个办法来转换成数组的下标
但是这个方法有一个问题假设我们这个字典的单词最长有 10 个字母组成, 那么字典的第一个单词 a 到 zzzzzzzzzz(10 个 z 的编码之和为 260) , 因此单词的编码范围是从 1 到 260, 数组根本容纳不了字典里 50000 个单词, 也许可以考虑每个数组元素包含一个子数组或链表, 但是这个办法严重降低了存取速度
因此, 这个方法把单词转化为数字的尝试还留下一些问题要解决, 比如有太多的单词需要用同一个数组下标, 例如, wastingivetendmoantickbailsdredge 和其他一百多个单词用编码求和都是 43, 和 cats 一样这个方法没有把单词分得足够开, 所以结果数组能表达的元素太少, 需要扩展数组表达下标的空间
A: 方法二: 幂的连乘
数字 7564 可以写成 10 的幂的连乘, 如下:
7 * 10 ^3 + 5 * 10 ^ 2 + 6 * 10 ^ 1 + 4 * 10 ^ 0
类似的方法, 可以把单词分解成字母组合, 每个字母的编码乘以 27 的幂, 然后将结果相加例如把单词 cats 转化为数字如下:
3 * 27 ^ 3 + 1 * 27 ^ 2 + 20 * 27 ^ 1 + 19 * 27 ^ 0 = 60337
这个过程可以为每个可能的单词创建一个独一无二的整数但是如果对于一个较长的单词会发生怎么样的事情呢? 最长的 10 个字母的单词 zzzzzzzzzz, 其结果转化为:
26*27^9 + 26*27^8 + 26*27^7 + 26*27^6 + 26*27^5 + 26*27^4 + 26*27^3 + 26*27^2 + 26*27^1 + 26*27^0
仅 27^9 就超过了 7,000,000,000,000, 所以可以看到这个结果非常巨大, 数组根本没有这么大的长度这个问题的出现是因为这个方案为每个可能的单词分配了一个数组元素, 不管这个单词是不是真正的英文单词, 因此数组元素就从 aaaaaaaaaa, aaaaaaaaab, aaaaaaaaac, 一直到 zzzzzzzzzz 如下图显示的情况,
A: 综上, 第一种方案产生的数组下标太少, 第二种方案产生的数组下标又太多
Q: 如何把巨大整数范围压缩到一个可接受的数组范围?
A: 针对只有 50000 个单词的字典, 可能会假设这个数组大概就有这么大的空间, 但实际上需要多一倍的空间容纳这些单词, 所以需要容量为 100000 长度的数组
A: 现在就得找一种方法, 把 0 到超过 7,000,000,000,000 的范围, 压缩到从 0 到 100000 有一种简单的方法是取余例如把从 0 到 199 的数字(用变量 hugeNumber 表示), 压缩为从 0 到 9 的数字, 压缩率为 20:1 如下图:
A: 回忆一下, 长度为 10 个字符的单词转化成数字的方法:
hugeNumber = ch0*27^9 + ch1*27^8 + ch2*27^7 + ch3*27^6 + ch4*27^5 + ch5*27^4 + ch6*27^3 + ch7*27^2 + ch8*27^1 + ch9*27^0
然后取余,
- arraySize = numberWords * 2;
- arrayIndex = hugeNumber % arraySize;
其中
arrayIndex = hugeNumber % arraySize
就是一种哈希函数它把一个大范围的数字哈希为一个小范围的数字, 这个小范围对应着数组的下标, 使用哈希函数向数组插入数据后, 这个数组就称为哈希表
A: 在这里 hugeNumber 可能会超出变量的范围, 即使变量类型为 long, 后面会看到如何处理这个问题
Q: 如何解决冲突?
A: 不可避免地把几个不同的单词哈希化到同一个数组元素中, 假设要在数组中插入单词 melioration, 通过哈希函数得到它的数组下标后, 发现这个元素已经有一个单词 demystify 了, 这种情况就称为冲突如下图,
A: 那么如何解决冲突呢?
A: 方法一, 开放地址法, 当冲突发生时, 通过系统的方法找到数组的一个空位(前面已经说过, 指定数组的大小两倍于需要存储的数据量), 把这个单词填入例如, 如果 cats 哈希化的结果是 5421, 但它的位置已经被 parsnip 占用, 那么可能会考虑把 cats 放在 5422 的位置上
A: 方法二, 链地址法, 创建一个存放单词链表的数组, 数组内不直接存储单词, 这样发生冲突时, 新的数据项直接到这个数组下标所指的链表里添加
Q: 开放地址法有哪些具体的方法?
A: 开放地址法有三种方法: 线性探测二次探测和再哈希法探测序列通常会用第三种方法
Q: 什么是线性探测(Linear Probing)?
A: 如果 5421 是要插入数据的位置, 它已经被占用了, 那么就使用 5422, 然后 5423, 依次类推, 数组下标一直递增, 直到找到空位, 这就叫做线性探测
A: 根据冲突的位置, 查找算法沿着数组一个一个地察看每个元素, 这个过程就叫探测, 如果在找到要查找的关键字前遇到一个空位, 说明查找失败, 不需要再做查找, 下图显示了成功和不成功的线性探测
Q: 什么叫聚集(Clustering)?
A: 在哈希表中, 一串连续的已填充元素叫做填充序列, 增加越来越多的数据项时, 填充序列变的越来越长, 这叫做聚集
Q: 如何设计一个好的哈希表?
A: 哈希表在几乎被填满的数组中添加数据项, 效率会非常低比如在 60 个长度已经容纳了 59 个数据项的哈希表中, 填入一个数据项都需要花费很长的时间而且应该注意如果哈希表被完全填满, 算法应该停止工作
当数据项数目占哈希表长的一半, 或最多到三分之二时, 哈希表的性能最好
A: 因此设计哈希表的关键要确保它不会超过整个数组容量的一半, 最多到三分之二(下文会继续讨论哈希表的装填数据的程度和探测长度的数学关系)
Q: 线性探测哈希表的 Java 代码?
A: 编程需实现查找插入和删除接口
A: https://gitee.com/firewaycoding/ds_in_java/tree/acbc069ff96b4e20fb708b6c142be1103fcd52b1/chapter11/01HashTable
A: 本程序中并没有考虑扩容数组的情况, 作为演示只是想你了解哈希表内部发生冲突时的场景, 应该考虑写一个 rehash()方法, 只要填充因子大于 0.5,insert()方法就会调用它, 把整个哈希表复制到比它大两倍的数组中这个过程叫重新哈希化, 这是一个耗时的过程, 但如果数组要进行扩展, 这个过程是必要的
Q: 如何防止聚集的产生?
A: 前面已经看到在开放地址法的线性探测中会发生聚集, 一旦聚集形成, 它就会变得越来越大, 那些哈希化后落在聚集范围内的数据项, 就得一步一步地移动, 直到插在聚集的最后这就像人群, 当某个人在商场晕倒, 人群就慢慢聚集, 最初的人聚过来是因为看到了那个倒下的人, 后面的人聚过来因为他们想知道每个人都在看什么人群聚得越大, 吸引的人就会越多
A: 聚集降低了哈希表的性能, 二次探测就是防止聚集产生的一种尝试
Q: 什么是装填因子(Load Factor)?
A: 已填入哈希表的数据项数和哈希表容量的比值有 10000 个元素的哈希表填入 6667 个数据后, 它的装填因子是 2/3
loadFactort = nItems / mArraySize;
当装填因子不太大时, 聚集分布得比较连贯哈希表的某个部分可能包含大量的聚集, 而另一个部分还很稀疏
Q: 什么是二次探测(Quadratic Probing)?
A: 二次探测的思路是探测相隔较远的元素, 而不是和原始位置相邻的元素
A: 步骤是步数的平方在线性探测中, 如果哈希函数计算的原始下标是 x, 线性探测就是 x+1, x+2, x+3, 依次类推而在二次探测中, 探测的过程是 x+1,x+2^2,x+3^2,x+4^2,x+5^, 依次类推如下图
A: 当二次探测的搜索变长时, 它变得越来越绝望第一次它查找相邻的元素, 如果这个元素被占用, 它认为这里可能有一个小的聚集, 所以它尝试距离为 4 的步长, 如果这里也被占用, 它变得有些焦虑, 认为这里有一个大的聚集, 然后尝试距离为 9 的步长, 如果这里还被占用, 它感到一丝恐慌, 调到距离为 16 的元素, 很快, 它会歇斯底里地飞跃完整个数组空间当哈希表几乎填满时, 就会出现这种情况
Q: 开放地址法为什么建议数组的容量为质数?
A: 数组容量总是选一个质数, 例如用 59 代替 60(小于 60 的质数有 59,53,47,43,41,37,31,29,23,19,17,13,11,7,5,3 和 2)如果数组容量不是质数, 在探测过程中, 步长序列就会变得无限长
A: 在下文的再哈希法中, 我们会看到, 再哈希法要求哈希表的容量必须是一个质数为什么要有这个限制, 假设表的容量不是质数例如, 假设表的大小是 15, 有一个特定的关键字映射到 0, 步长为 5. 探测序列就是 0,5,10,0,5,10, 依次类推, 一直循环下去算法只尝试这三种元素, 最终会导致程序崩溃
如果数组容量是 13, 即一个质数, 再哈希法探测序列最终会访问所有元素, 即 0,5,10,2,7,12,4,9,1,6,11,3 一直下去只要表中有一个空位, 就可以探测到用质数作为数组容量是的任何数想整除它都是不可能的, 因此探测序列最终会检查到所有元素
Q: 二次探测的问题?
A: 二次探测消除了线性探测中产生的聚集问题, 这种聚集问题叫做主要聚集 (primary clustering) 然而, 二次探测产生了另外一种更细的聚集问题是因为所有映射到同一个位置的关键字在寻找空位时, 探测的元素是一样的
A: 比如, 在大小长度为 59 的数组里, 将 184,302,420 和 544 依次插入到表中, 它们都映射到 7, 那么 302 将会以 1 为步长进行探测, 420 将会以 4 为步长进行探测, 544 则需要 9 为步长的探测只要有一项, 其关键字映射到 7, 就需要更长步长的探测, 这个现象就叫做次要聚集(secondary clustering)
A: 二次探测不会被经常, 因为还有稍微好些的解决方案
Q: 什么是再哈希法?
A: 为了消除主要聚集和次要聚集, 可以使用另外的一个方法: 再哈希法
A: 次要聚集产生的原因是产生的探测步长总是固定的: 1,4,9,16,... 为此, 为了使步长不是那么固定, 第二个哈希函数对关键字再做一遍哈希化
A: 经验说明, 第二个哈希函数必须具备如下特点:
1) 和第一个哈希函数不同
2) 不能输出 0(否则每次探测都是原地踏步, 算法将陷入死循环)
第二个哈希函数取下面的形式会比较好
step = constant - (key % constant);
其中 constant 是质数, 且小于数组容量
例如, step = 5 - (key % 5), 用这个函数, 步长范围是从 1 到 5
Q: 再哈希法的 Java 代码?
A:https://gitee.com/firewaycoding/ds_in_java/tree/acbc069ff96b4e20fb708b6c142be1103fcd52b1/chapter11/02HashDoubleTable
A: 测试用例的 test2()显示了当 21 个数据项插入到容量为 23 的哈希表的情况, 处理冲突的方法使用再哈希法, 步长从 1 到 5 如下图:
前面 15 个关键字大多数映射到空白元素(第 10 个是不规则的), 这以后, 随着数组越来越满, 探测序列也越来越长如下图:
Q: 什么是链地址法(Separate Chaining)?
A: 映射到同一个位置的数据项只需要加到链表中, 而不需要在原始数组中寻找空位如下图,
Q: 链地址法的 Java 代码?
A: HashChainTable 程序主要难点是在于有序链表的插入或删除操作
A: testInsertCase1(), 插入第 1 个元素的场景
A: testInsertCase2(), 插入第 2 个元素, 比第 1 个元素大
A: testInsertCase3(), 插入第 2 个元素, 比第 1 个元素小
A: testInsertCase4(), 插入第 3 个元素, 比第 1 个元素大, 比第 2 个元素小
A: testInsertCase5(), 插入重复元素注意
current.getKey() <= key
与
current.getKey() < key
的区别
A: 同理删除
Q: 链地址法的装填因子?
A: 链地址法中的装填因子与开放地址法的不同, 在链地址法中, 可以比 1 大
A: 当然如果链表中有许多项, 存取时间会变长, 因为存取特定数据项的平均需要搜索链表的一半的数据项但总体上比开放地址法要好一些, 因为开放地址法中, 当装填因子超过 1/2 或 2/3 之后, 性能下降得很快, 在链地址法中, 装填因子可以达到 1 以上, 且对性能影响不大
Q: 设计一个好的哈希函数的因素需要哪些?
A: 好的哈希函数的要求就只有一个, 就是能快速地计算, 如果哈希函数运行缓慢, 速度就会降低哈希函数中有许多乘法和除法是不可取的, Java/C/C++ 语言可以使用位操作, 例如可以使用右移来代替除以 2 的倍数
A: 哈希函数的目的是使得较大的关键字范围压缩成较小的数组下标的范围, 因此方法最好能够使关键字随机地分布在整个哈希表中
A: 对于哈希函数 index = key % arraySize; 如果关键字是一个真随机数, 得到的下标也是随机数, 就会得到一个良好的分布情况然而, 关键字通常不是随机数
A: 例如, 居民身份证编码就不是随机数, 根据中华人民共和国国家标准 GB11643-1999 中有关公民身份号码的规定, 公民身份号码是特征组合码, 由十七位数字本体码和一位数字校验码组成排列顺序从左至右依次为: 六位数字地址码, 八位数字出生日期码, 三位数字顺序码和一位数字校验码
例如, 430181198506228889, 各个数字的含义如下:
1) 0-1 位: 省份(43 代表湖南)
2) 2-3 位: 地区级城市(01 代表长沙)
3) 4-5 位: 县级市(81 代表浏阳市)
4) 6-9 位: 出生年(1985 年)
5) 10-11 位: 出生月(06 月)
6) 12-13 位: 出生日(22 日)
7) 14-16 位: 县区级政府所辖派出所的分配码, 其中单数为男性分配码, 双数为女性分配码如遇同年同月同日有两个人以上顺延第二第三第四第五个等分配码, 如 007 的就是个男生, 而且和他同年月日的男生至少有 001003005, 那下一个同年月日的男生就是 009
8) 17 位: 校验码, 主要是为了校验计算机输入公民身份证前 17 位数字是否正确, 其取值 0~10, 当值等于 10 时, 用罗马数字符 X 表示其计算方法如下:
将前面的身份证号码 17 位数分别乘以不同的系数从第 0 位到第 16 位的系数分别为: 7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2 然后将这些乘积相加, 所得的结果除以 11, 看余数是多少余数对应的最后一位身份证的号码如下图:
A: 压缩关键字时, 要把每个位都计算在内, 但是校验码应该舍弃, 因为它没有提供任何有用的信息, 在压缩中是多余的
A: 关键字提供的数据越多, 哈希化后, 越可能覆盖整个下标范围
Q: 哈希化字符串?
A: 前面我们已经介绍对于长度为 4 的字符串 cats 转化为数字下面是哈希化的代码:
- public static int hashFunc1(String key)
- {
- int hash = 0;
- int pow27 = 1; // 1, 27, 27*27, etc
- for(int i=key.length()-1; i>=0; i--)
- {
- int letter = key.charAt(i) - 96; // get char code
- hash += pow27 * letter; // times power of 27
- pow27 *= 27; // next power of 27
- }
- return hash % arraySize;
- }
hashFunc1()方法不如想象的那么有效, 除了字符转换外, 在循环中有两次相乘和一次相加还有另一方法可以取代
这个方法叫 Horner 方法(Horner 是英国数学家, 1773-1827)
从最内侧的括号开始, 逐渐向外运算, java 代码如下:
- public static int hashFunc2(String key) {
- int hash = key.charAt(0) - 96;
- for (int i =1; i < key.length(); i++) {
- int letter = key.charAt(i) - 96;
- hash = hash * 27 + letter;
- }
- return hash % arraySize;
- }
不行的是, hashFunc2()方法不能处理大于 7 个字符的字符串, 否则会超出 int 类型的范围, 可以使用下面的方法取代:
- public static int hashFunc2(String key) {
- int hash = 0;
- for (int i =1; i < key.length(); i++) {
- int letter = key.charAt(i) - 96;
- hash = (hash * 27 + letter) % arraySize;
- }
- return hash;
- }
这种方法或类似的方法通常用来哈希化字符串, 也可以应用不同的位操作技巧, 例如使用 32 或者更大的 2 的幂作为模取代 32, 这样乘法可以结合右移增加效率, 右移比取模更快
Q: 哈希化的效率?
A: 哈希表一次单独的查找和插入时间与探测的长度成正比, 还要加上哈希函数的常量时间因此探测长度取决于装填因子, 随着装填因子变大, 探测长度也越来越长
A: 下面会看到不同的哈希表中, 探测长度和装填因子之间的关系
A: 开放地址法
线性探测
随着装填因子变大, 效率下降, 比链地址法更严重
不成功查找比成功查找花费更多的时间, 在探测序列中, 只要找到要目标位置, 算法就能停止, 平均起来, 就会发生探测序列的一半, 另一方面, 要确定不能找到这样的目标位置, 就必须走过整个探测序列
下面的等式显示了线性探测时, 探测序列 (P) 与装填因子 (L) 的关系这个公式来自 Knuth, 这些推导出来相当复杂
对成功的查找来说
而对于不成功的查找来说
下图显示了用坐标表示这个等式的结果
当装填因子为 0.5 时, 成功的搜索需要 1.5 次比较, 不成功的搜索需要 2.5 次
当装填因子为 2/3 时, 分别需要 2.0 次和 5.0 次比较如果装填因子更大, 比较次数会变得非常大
正如我们看到, 应该使装填因子保持在 2/3 以下, 最好在 1/2 以下另一方面, 装填因子越低, 对于给定数量的数据项, 就需要更多的空间实际情况中, 最好的装填因子取决于存储效率和速度的平衡随着装填因子变小, 存储效率下降, 而速度上升
二次探索和再哈希法
二次探测和再哈希法的性能相当, 比线性探测略好
对于成功的搜索, 公式如下(公式仍然来自 Knuth):
-log2(1 - loadFactor) / loadFactor
对于不成功的查找, 公式如下:
1 / (1 - loadFactor)
当装填因子为 0.5 时, 成功和不成功的查找都平均需要两次比较, 当装填因子为 2/3 时, 分别需要 2.37 和 3.0 次比较, 当装填因子为 0.8 时, 分别需要 2.9 和 5.0 次
A: 链地址法
假设哈希表包含 mHashArraySize 个数据项, 每个数据项有一个链表, 哈希表中有 N 个数据项那么平均起来每个链表的数据项如下:
sortedListSize = N / mHashArraySize
这和装填因子的定义是相同的:
loadFactor = N / mHashArraySize
所以平均表长等于装填因子
查找
对于成功查找, 平均起来找到正确项之前, 要检查一半的数据项, 因此查找时间
1 + loadFactor / 2
不管链表是否有序, 都遵循这个公式
对于不成功查找, 如果链表不是有序的, 要检查所有的数据项, 所以时间是
1 + loadFactor
在链地址法中, 通常装填因子为 1(数据项的个数和数组容量相同)较小的装填因子不能显著地提升性能但是如果所有操作的时间都会随着装填因子的变大而增大, 所以不宜把装填因子提升到 2
插入
如果链表是无序的, 插入操作是立即完成的, 这里不需要比较所以插入的时间为 1;
如果链表是有序的, 那么, 由于存在不成功的查找, 平均要检查一半的数据项, 所以插入的时间是
1 + loadFactor / 2
Q: 开放地址法和链地址法的比较?
A: 对于小型的哈希表, 如果使用开放地址法, 再哈希法似乎比二次探测的效果好但是前提是内存一定要充足, 并且哈希表一经创建, 就不在改变其容量, 在这种情况下, 线性探测相对容易实现, 并且如果装填因子低于 0.5, 几乎没有什么性能下降
如果在哈希表创建时, 要填入的项数未知, 链地址法要好过开放地址法
当两者都可选时, 使用链地址法它需要使用链表类, 但回报是增加比预期更多的数据时, 不会导致性能快速下降
Q: 外部存储使用哈希表的场景?
A: 前面我们已经介绍了外部存储关于索引的内容, 索引可以使用哈希表来存储的文件索引是由关键字 - 块号码组成的列表, 它按关键字排序, 这个索引也可以叫做文件指针表
这里还是使用外部存储电话本的例子, 块大小为 8192 字节, 一个记录为 512 字节, 因此可以一个块可以容纳 16 个记录哈希表的每个元素指向了某个块假设这个电话本有 100 个块第一个块为 0, 一直到 99
我们把电话本的名字作为关键字哈希化到哈希表的下标, 在外部哈希化中, 重要的是块不要填满, 因此每个块平均存储 8 个记录有的快可能多些, 有的少些
在本例中中大概有 800 个记录, 排列如下图,
所有关键字映射为同一个值的记录都定位到相同块
为找到特定关键字的记录, 搜索算法哈希化关键字, 用哈希值作为哈希表的下标, 得到某个下标中的块号, 然后读取这个块
为了实现这个方案, 必须仔细选择哈希函数和哈希表的大小, 为的是限制映射到相同的值关键字的数量在本例中, 平均每个值只对应 8 个记录
即使用一个好的哈希函数, 块偶尔也会填满, 这时, 可以使用在内部哈希表中讨论的处理冲突的不同方法: 开放地址法和链地址法
在开放地址法中, 插入时, 如果发现一个块是满的, 算法在相邻的块插入新记录在线性探测中, 这时下一个块, 但也可以使用二次探测或再哈希法选择在链地址法中, 有一个溢出块, 当发现块已满时, 新记录插在溢出块中
Q: 本篇总结?
哈希表基于数组
关键字值的范围通常比数组容量大
关键字值通过哈希函数映射为数组的下标
英文字典是一个数据库的典型案例, 它可以有效的用哈希表来处理
一个关键字哈希化到已占用的数组单元, 这种情况叫做冲突
冲突可以用两种方法解决: 开放地址法和链地址法
在开放地址法中, 把冲突的数据项放在数组的其他位置
在链地址法中, 每个数组元素包含一个链表, 把所有映射到同一个数组下标的数据项都插入这个链表中
讨论了三种开放地址法: 线性探测二次探测和再哈希化
在线性探测中, 步长总是 1, 所以如果 X 是哈希函数计算得出的数组下标, 那么探测序列就是 X, X+1, X+2, X+3, 以此类推
找到一个特定项需要经过的步数叫做探测长度
在线性探测中, 已填充单元的长度不断增加它们叫做主要聚集(primary clustering), 这会降低哈希表的性能
在二次探测中, X 的位移是步数的平方, 所以探测序列就是 X, X+1, X+4, X+9, X+16 一次类推
二次探测消除了主要聚焦, 但是产生了次要聚集, 它比主要聚集的危害略小
二次聚集的发生时因为映射到同一个单元的关键字, 在探测过程中执行了相同的序列 发生上述情况是因为步长只依赖于哈希值, 与关键字无关
在再哈希法中, 步长依赖于关键字, 且从第二个哈希函数中得到
装填因子是表中数据项数和数组容量的比值
在开放地址法中, 当装填因子接近 1 时, 查找时间趋于无限
在开放地址法中, 关键是哈希表不能填得太满
对于链地址法, 装填因子为 1 比较合适
字符串可以这样哈希化, 每个字符乘以常数的不同次幂, 求和, 然后用取模操作符 (%) 缩减结果, 以适应哈希表的容量
哈希表的容量通常是一个质数, 这在二次探测和再哈希法中非常重要
哈希表可用于外部存储, 一种做法是用哈希表的单元存储磁盘文件的块号码
参考
Java 数据结构和算法第 11 章 - 哈希表
来源: http://www.bubuko.com/infodetail-2513680.html