【数据结构与算法学习】散列表(Hash Table,哈希表)
实现语言:C++
1. 散列表
散列表,英文名称为Hash Table,又称哈希表、杂凑表等。
线性表和树表的查找是通过比较关键字的方法,查找的效率取决于关键字的比较次数。
而散列表是根据关键字直接访问的数据结构。散列表通过散列函数将关键字映射到存储地址,建立了关键字和存储地址之间的一种直接映射关系。
例如:关键字集key = (17, 24, 48, 25),散列函数H(key) = key % 5,散列函数将关键字映射到存储地址下标,将关键字存储到散列表的对应位置。
理想情况下,散列表查找的时间复杂度是O(1)。但是,散列函数可能会把两个或两个以上的关键字映射到同一地址,发生“冲突”,发生冲突的不同关键字称为“同义词”,也就是具有相同函数值的关键字。
接上例,如果13也要存入散列表,就会和48产生冲突:
所以,使用散列表需要解决好两个问题:构造合适的散列函数,以及制定一个好的解决冲突的方案。
2. 散列函数的构造方法
在构造散列函数时需要考虑诸多因素:执行速度、关键字长度、散列表大小、关键字的分布情况、查找频率等。
根据元素集合的特性构造,我们对散列函数有以下要求:
- n的数据源仅占n个地址,虽然散列查找是以空间换时间,但仍希望散列的地址空间尽量小。
- 无论用什么方法存储,目的都是尽量均匀地存放元素,以避免冲突。
常见的构造方法有:直接定址法、除留余数法、数字分析法、平方取中法、折叠法、随机数法。
2.1 直接定址法
以关键码key的某个线性函数值作为散列地址
Hash(key) = a * key + b (a,b为常数)
优点是线性关系,不会产生冲突。但是要占用连续的地址空间,空间效率低下。
适合查找表较小且连续的情况。
例如:使用直接定址法存储序列 {100, 300, 500, 700, 800, 900},选择散列函数 Hash(key) = key / 100 (a=1/100, b=0)
2.2 除留余数法
此方法是最常用的散列函数构造方法
Hash(key) = key mod p (p是一个整数)
关键:如何选取合适的p?
技巧:设表长为m,取p≤m,且p为质数
例:{15, 23, 27, 38, 53, 61, 70},散列函数 Hash(key) = key mod 7
2.3 数字分析法
如果关键字是位数较多的数字(比如手机号),且这些数字部分存在相同规律,则可以采用抽取剩余不同规律部分作为散列地址。
比如手机号前三位是接入号,中间四位是 HLR 识别号,只有后四位才是真正的用户号。也就是说,如果手机号作为关键字,那么极有可能前 7 位是相同的,此时我们选择后四位作为散列地址就是不错的选择。同时,对于抽取出来的数字,还可以再进行反转、右环位移、左环位移等操作,目的就是为了提供一个能够尽量合理地将关键字分配到散列表的各个位置的散列函数。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。
2.4 平方取中法
以关键字平方的中间位数作为散列地址。
比如假设关键字是 4321,那么它的平方就是 18671041,抽取中间的 3 位就可以是 671,也可以是 710,用做散列地址。
适合于不知道关键字的分布,而位数又不是很大的情况。
2.5 折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
比如假设关键字是 9876543210,散列表表长为三位。
有时可能这还不能够保证分布均匀,那么也可以尝试从一端到另一端来回折叠后对齐相加,比如将 987 和 321 反转,再与 654 和 0 相加,变成 789+654+123+0=1566,此时散列地址为 566。
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
2.6 随机数法
选择一个随机数,取关键字的随机函数值作为它的散列地址:
hash(key) = random(key)
当关键字的长度不等时采用这个方法构造散列函数是比较适合的。
3. 处理哈希冲突的方法
3.1 开放定址法(开地址法)
基本思想:有冲突时就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将数据元素存入。
常用的开放定址法有线性探测法、二次探测法、伪随机探测法等。
3.1.1 线性探测法
Hi = (Hash(key) + di) mod m,(1 ≤ i ≤ m)
其中,m为散列表长度,di = i(i为1,2,...,m-1 线性序列)
例:关键码集为{47, 7, 29, 11, 16, 92, 22, 8, 3},散列表长度为m=11,散列函数为Hash(key)=key mod 11,使用线性探测法处理冲突,存入过程如下:
3.1.2 二次探测法
增量序列di为12,-12,22,-22,...,q2 二次序列
同样是上边的例子,使用二次探测法处理冲突,存入过程如下:
注意:二次探测法是跳跃式探测,效率较高,但是会出现命名有有空间却探测不到的情况,因而存储失败,而线性探测只要有空间就一定能探测到。
3.1.3 伪随机探测法
增量序列di为伪随机数
其存入原理和上边两种方法一致,这里不多做介绍。
3.2 链地址法(拉链法)
基本思想:将相同散列地址的记录(即同义词)链成一单链表。
m个散列地址就是m个单链表,然后用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构。
例如:关键字为{19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79},散列函数为Hash(key) = key mod 13,使用链地址法存储如下所示:
链地址法建立散列表的步骤:
- 取数组元素的关键字key,计算其散列函数值(地址)。若该地址对应的链表为空,则将该元素插入此链表;否则执行下一步解决冲突;
- 根据选择的冲突处理方式,计算关键字key的下一个存储地址。若改地址对应的链表不为空,则利用链表的前插法或后插法将该元素插入此链表。
优点:
- 非同义词(同义词是指具有相同函数值的关键字)不会冲突,无“聚集”现象;
- 链表上结点空间动态申请,更适合于表长不确定的情况
3.3 再散列法
就是同时构造多个不同的哈希函数:
Hi = Hashi (key) i= 1,2,3 ... k;
当H1 = Hash1 (key) 发生冲突时,再用H2 = Hash2 (key) 进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。
3.4 建立一个公共溢出区
将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一存放到溢出区。
4. 散列表的查找
给定查找值k,查找过程如图所示:
散列表的查找效率分析
一般我们使用平均查找长度ASL来衡量查找效率,散列表ASL的值取决于:散列函数、处理冲突的方法、散列表的装填因子α(α = 表中填入的记录数 / 哈希表的长度,α越大,表中记录数越多,发生冲突的可能性就越大,查找对比次数就越多)。
线性探测法:ASL ≈ 1 / 2 * (1 + 1 / (1 - α))
拉链法:ASL ≈ 1 + α / 2
随机探测法:ASL ≈ -1 / α * ln(1 - α)
例:对于关键字集{19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79},n=12,散列函数为:H(key) = key mod 13,散列表表长为m = 16,设每个记录的查找概率相等。则使用不同查找算法的平均查找效率如下:
线性探测法:ASL ≈ 1 / 2 * (1 + 1 / (1 - 0.75)) = 2.5 (装填因子α = n / m = 0.75)
拉链法:ASL ≈ 1 + 0.75 / 2 = 1.375
随机探测法:ASL ≈ -1 / α * ln(1 - α) = 1.85
对比无序表查找和有序表折半查找:
无序表:ASL = (n +1) / 2 = 6.5
有序表折半查找:ASL = lg2(n + 1) - 1 = 2.7
总结以下几点:
- 散列表技术具有很好的平均性能;
- 链地址法优于开地址法;
- 除留取余法做散列函数优于其它类型函数。
C++代码
#include <iostream>
#include <cstring> using namespace std; #define m 15 // 哈希表的表长
#define NULLKEY 0 // 单元为空的标记 int HT[m], HC[m]; // 哈希函数
int H(int key) {
return key % 13;
} // 线性探测
int LineDetect(int HT[], int H0, int key, int& cnt) {
int Hi;
for (int i = 0; i < m; i++) {
cnt++;
Hi = (H0 + i) % m; // 按照线性探测法计算下一个哈希地址Hi
if (HT[Hi] == NULLKEY || HT[Hi] == key)
return Hi; // 若单元Hi为空,则所查元素不存在
}
return -1;
} // 二次探测
int SecondDetect(int HT[], int H0, int key, int& cnt) {
int Hi;
for (int i = 1; i <= m / 2; ++i) {
int i1 = 1 * i;
int i2 = -i1;
cnt++;
Hi = (H0 + i1) % m; // 按照二次探测法去计算下一个哈希地址
if (HT[Hi] == NULLKEY || HT[Hi] == key) // 若单元Hi为空或者查找成功
return Hi;
cnt++;
Hi = (H0 + i2) % m; // 按照二次探测法去计算下一个哈希地址
if (Hi < 0)
Hi += m;
if (HT[Hi] == NULLKEY || HT[Hi] == key) // 若单元Hi为空或者查找成功
return Hi;
}
return -1;
} // 哈希表中查找关键字key
void SearchHash(int HT[], int key) {
int H0 = H(key); // 计算哈希地址
int Hi, cnt = 1;
if (HT[H0] == NULLKEY)
cout << "查找失败" << endl;
else if (HT[H0] == key)
cout << "查找成功。" << "在第" << H0 + 1 << "位置。" << "比较次数:" << cnt << endl;
else {
Hi = LineDetect(HT, H0, key, cnt);
// Hi = SecondDetect(HT, H0, key, cnt);
if (HT[Hi] == key)
cout << "查找成功。" << "在第" << H0 + 1 << "位置。" << "比较次数:" << cnt << endl;
else
cout << "查找失败。比较次数:" << cnt << endl;
}
} // 插入元素
bool InsertHash(int HT[], int key) {
int H0 = H(key); // 根据哈希函数H(key)计算哈希地址
int Hi, cnt = 1;
if (HT[H0] == NULLKEY) {
HC[H0] = 1; // 统计比较次数
HT[H0] = key; // 放入H0中
return 0;
}
else {
Hi = LineDetect(HT, H0, key, cnt);
// Hi = SecondDetect(HT, H0, key, cnt);
if (Hi != -1 && HT[Hi] == NULLKEY) {
HC[Hi] = cnt; // 统计比较次数
HT[Hi] = key; // 放入H0中
return 1;
}
}
return 0;
} void print(int HT[]) {
for (int i = 0; i < m; i++)
cout << HT[i] << "\t";
cout << endl;
} int main() {
int x;
memset(HT, 0, sizeof(HT));
memset(HC, 0, sizeof(HC));
print(HT);
cout << "输出12个关键字,存入哈希表中:" << endl; for (int i = 0; i < 12; i++) {
cin >> x;
if (!InsertHash(HT, x)) {
cout << "创建哈希表失败!" << endl;
return 0;
}
}
cout << "输出哈希表:" << endl;
print(HT);
print(HC);
cout << "输入要查找的关键字" << endl;
cin >> x;
SearchHash(HT, x);
return 0;
} // 测试数据1:14 36 42 38 40 15 19 12 51 68 34 25
// 测试数据2:14 36 42 38 40 15 19 12 51 68 34 18
参考资料
2. 数据结构—— 构造散列函数的六种方法【直接定址法-数字分析法-平方取中法-折叠法-除留余数法-随机数法】
3. 哈希冲突常用解决方法
4. 书籍:算法训练营
【数据结构与算法学习】散列表(Hash Table,哈希表)的更多相关文章
- 算法导论-散列表(Hash Table)-大量数据快速查找算法
目录 引言 直接寻址 散列寻址 散列函数 除法散列 乘法散列 全域散列 完全散列 碰撞处理方法 链表法 开放寻址法 线性探查 二次探查 双重散列 随机散列 再散列问题 完整源码(C++) 参考资料 内 ...
- 白话算法(6) 散列表(Hash Table)从理论到实用(中)
不用链接法,还有别的方法能处理碰撞吗?扪心自问,我不敢问这个问题.链接法如此的自然.直接,以至于我不敢相信还有别的(甚至是更好的)方法.推动科技进步的人,永远是那些敢于问出比外行更天真.更外行的问题, ...
- 白话算法(6) 散列表(Hash Table)从理论到实用(上)
处理实际问题的一般数学方法是,首先提炼出问题的本质元素,然后把它看作一个比现实无限宽广的可能性系统,这个系统中的实质关系可以通过一般化的推理来论证理解,并可归纳成一般公式,而这个一般公式适用于任何特殊 ...
- 白话算法(6) 散列表(Hash Table) 从理论到实用(下)
[澈丹,我想要个钻戒.][小北,等等吧,等我再修行两年,你把我烧了,舍利子比钻戒值钱.] ——自扯自蛋 无论开发一个程序还是谈一场恋爱,都差不多要经历这么4个阶段: 1)从零开始.没有束缚的轻松感.似 ...
- [转载] 散列表(Hash Table)从理论到实用(中)
转载自:白话算法(6) 散列表(Hash Table)从理论到实用(中) 不用链接法,还有别的方法能处理碰撞吗?扪心自问,我不敢问这个问题.链接法如此的自然.直接,以至于我不敢相信还有别的(甚至是更好 ...
- [转载] 散列表(Hash Table)从理论到实用(上)
转载自:白话算法(6) 散列表(Hash Table)从理论到实用(上) 处理实际问题的一般数学方法是,首先提炼出问题的本质元素,然后把它看作一个比现实无限宽广的可能性系统,这个系统中的实质关系可以通 ...
- [转载] 散列表(Hash Table) 从理论到实用(下)
转载自: 白话算法(6) 散列表(Hash Table) 从理论到实用(下) [澈丹,我想要个钻戒.][小北,等等吧,等我再修行两年,你把我烧了,舍利子比钻戒值钱.] ——自扯自蛋 无论开发一个程序还 ...
- 散列表(Hash Table)
散列表(hash table): 也称为哈希表. 根据wikipedia的定义:是根据关键字(Key value)而直接访问在内存存储位置的数据结构.也就是说,它通过把键值通过一个函数的计算,映射到表 ...
- Java 集合 散列表hash table
Java 集合 散列表hash table @author ixenos 摘要:hash table用链表数组实现.解决散列表的冲突:开放地址法 和 链地址法(冲突链表方式) hash table 是 ...
- 散列表(Hash table)及其构造
散列表(Hash table) 散列表,是根据关键码值(Key value)而直接进行访问的数据结构.它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度.这个映射函数叫做散列函数,存放记录 ...
随机推荐
- .Net 6.0服务器环境配置以及IIS发布部署心得
经过长达一个月的时间,对.Net 后端开发后的发布以及服务器域名等网站搭建全部过程留档,也帮助有需要的同志进行学习以及查看 https://www.cnblogs.com/xingmengcode/a ...
- A Super Hero
DP? QwQ这题似乎不能直接贪心2333-- 阶段 很明显的阶段性,\(n\)关便为\(n\)个阶段, 状态 分好阶段后,容易构造出状态的表达: \(f[i,j]\)表示Ma5termind在最开始 ...
- 新手:git回滚代码,合并代码解决冲突
回滚到某个tag: git reset --hard 70438034dc git push -f 合并---解决冲突---提交: Step 1. Fetch and check out the br ...
- PNETLab添加锐捷镜像后无法使用telnet
PNETLab 版本: 4.2.10 锐捷镜像版本: V1.03 故障详情: 使用PNETLab添加锐捷交换机.路由器镜像后,在Lab中添加设备,默认打开方式为telnet,telnet客户端无论是s ...
- 身为一个java开发人员对目前java开发的一些小小的认识
为什么要学java? 第一要工作,第二想要高薪 首先身为java开发工作人员你的思想认知程度要搞,思想认知程度决定你的专业技能水平 JavaSE工程师 B/S 架构 ---- 浏览器 ...
- mockjs 加上 json-server 快速生成前端数据
const mock = require('mockjs'); // 引入mockjs const data = mock.mock({ "data|20": [{ "i ...
- C++ 复习函数的基本知识
C++ 复习函数的基本知识 要使用 C++ 函数,必须完成如下工作: 1. 提供函数定义: 2. 提供函数原型: 3. 调用函数. 例子: #include <iostream> usin ...
- 2月27日Android开发学习
App工程目录结构 App工程分为两个层次,第一个层次是项目,另一个层次是模块. 模块依附于项目,每个项目至少有一个模块.一般而言的"编译运行App",指的是运行某一模块,而非运行 ...
- Ajax属性
如何创建一个 Ajax <script> window.onload=function(){ 1)创建一个异步调用对象 va ...
- javaweb本地启动很快,服务器上面启动特别慢
在JVM环境中解决 打开$JAVA_PATH/jre/lib/security/java.security这个文件,找到下面的内容: securerandom.source=file:/dev/ura ...