没有合适的资源?快使用搜索试试~ 我知道了~
哈希表的应用,介绍了一些哈希表的用例
4星 · 超过85%的资源 需积分: 28 10 下载量 111 浏览量
2011-04-07
09:45:00
上传
评论
收藏 291KB PDF 举报
温馨提示
试读
22页
介绍了一些哈希表的典型应用例子,对自己打基础很有帮助,哈希表是一些算法经常会用到得工具
资源推荐
资源详情
资源评论
竞赛中哈希表的应用
简介:
哈希(Hash)表
前面讲的查找方法是基于比较的方法,查找效率依赖比较次数,其实理想的查找是希望不经比较,一次存取便
能得到所查记录。这样就必须在记录的存储位置和它的关键字之间建立一个确定的对应关系 f,查找 k 时,只要根据
这个对应关系 f 找到给定值 k 的像 f(k)。这种对应关系 f 叫哈希(hash)函数。按这种思想建立的表叫哈希表(也叫
散列表)。
哈希表存取方便但存储时容易冲突(collision):即不同的关键字可以对应同一哈希地址。如何确定哈希函数和
解决冲突是哈希表查找的关键。
1.哈希函数的构造方法
构造哈希函数的方法有很多,这里介绍几种常用的。
直接定址法:H(k)=k 或 H(k)=a*k+b(线形函数)
如:人口数字统计表
地址 1 2 3 ... 100
年龄 1 2 3 ... 100
人数 67 3533 244 ... 4
数字分析法:取关键字的若干数位组成哈希地址
如:关键字如下:若哈希表长为 100 则可取中间两位 10 进制数作为哈希地址。
81346532 81372242 81387422 81301367 81322817 81338967 81354157 81368537
平方取中法:关键字平方后取中间几位数组成哈希地址
折叠法:将关键数字分割成位数相同的几部分(最后一部分的位数可以不同)然后取几部分的叠加和(舍去进
位)作为哈希地址。
除留余数法:取关键字被某个不大于表长 m 的数 p 除后所得的余数为哈希地址。
H(k)=k mod p p<=m
随机数法:H(k)=rondom(k)。
2.处理冲突的方法
假设地址集为 0..n-1,由关键字得到的哈希地址为 j(0<=j<=n-1)的位置已存有记录,处理冲突就是为该关键字的记
录找到另一个"空"的哈希地址。在处理中可能得到一个地址序列 Hi i=1,2,...k 0<=Hi<=n-1),即在处理冲
突时若得到的另一个哈希地址 H1 仍发生冲突,再求下一地址 H2,若仍冲突,再求 H3...。怎样得到 Hi 呢?
开放定址法:Hi=(H(k)+di) mod m (H(k)为哈希函数;m 为哈希表长;di 为增量序列)
当 di=1,2,3,... m-1 时叫线性探测再散列。
当 di=1
2
,-1
2
,2
2
,-2
2
,3
2
,-3
2
,...,k
2
,-k
2
时叫二次探测再散列。
当 di=random(m)时叫伪随机探测序列。
例:长度为 11 的哈希表关键字分别为 17,60,29,哈希函数为 H(k)=k mod 11,第四个记录的关键字为 38,分
别按上述方法添入哈希表的地址为 8,4,3(随机数=9)。
再哈希法:Hi=RHi(key) i=1,2,...,k,其中 RHi 均为不同的哈希函数。
链地址法:这种方法很象基数排序,相同的地址的关键字值均链入对应的链表中。
建立公益区法:另设一个溢出表,不管得到的哈希地址如何,一旦发生冲突,都填入溢出表。
3.哈希表的查找
例:如下一组关键字按哈希函数 H(k)=k mod 13 和线性探测处理冲突所得的哈希表 a[0..15]:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
14
01
68
27
55
19
20
84
79
23
11
10
当给定值 k=84,则首先和 a[6]比,再依次和 a[7],a[8]比,结果 a[8]=84 查找成功。
当给定值 k=38,则首先和 a[12]比,再和 a[13]比,由于 a[13]没有,查找不成功,表中不存在关键字等于 38 的记录。
哈希表是一种高效的数据结构。以下分五个部分:首先提出了哈希表的优点,其次介绍了它的基础操作,接着
从简单的例子中作了效率对比,指出其适用范围以及特点,然后通过例子说明了如何在题目中运用哈希表以及需要
注意的问题,最后总结全文。
[正文]
1. 引言
哈希表(Hash Table)的应用近两年才在 NOI 中出现,作为一种高效的数据结构,它正在竞赛中发挥着越来越
重要的作用。
哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是
消耗比较多的内存。然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。另外,编码比较容
易也是它的特点之一。
哈希表又叫做散列表,分为"开散列" 和"闭散列"。考虑到竞赛时多数人通常避免使用动态存储结构,本文中的
"哈希表"仅指"闭散列",关于其他方面读者可参阅其他书籍。
2. 基础操作
2.1 基本原理
我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每
个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解
为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地方。
但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了
相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种
解决"冲突"的简便做法。
总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。
2.2 函数构造
构造函数的常用方法(下面为了叙述简洁,设 h(k) 表示关键字为 k 的元素所对应的函数值):
a) 除余法:
选择一个适当的正整数 p ,令 h(k ) = k mod p
这里, p 如果选取的是比较大的素数,效果比较好。而且此法非常容易实现,因此是最常用的方法。
b) 数字选择法:
如果关键字的位数比较多,超过长整型范围而无法直接运算,可以选择其中数字分布比较均匀的若干位,所组
成的新的值作为关键字或者直接作为函数值。
2.3 冲突处理
线性重新散列技术易于实现且可以较好的达到目的。令数组元素个数为 S ,则当 h(k) 已经存储了元素的时候,
依次探查 (h(k)+i) mod S , i=1,2,3…… ,直到找到空的存储单元为止(或者从头到尾扫描一圈仍未发现空单元,
这就是哈希表已经满了,发生了错误。当然这是可以通过扩大数组范围避免的)。
2.4 支持运算
哈希表支持的运算主要有:初始化(makenull)、哈希函数值的运算(h(x))、插入元素(insert)、查找元素
(member)。
设插入的元素的关键字为 x ,A 为存储的数组。
初始化比较容易,例如
const empty=maxlongint; // 用非常大的整数代表这个位置没有存储元素
p=9997; // 表的大小
procedure makenull;
var i:integer;
begin
for i:=0 to p-1 do
A[i]:=empty;
End;
哈希函数值的运算根据函数的不同而变化,例如除余法的一个例子:
function h(x:longint):Integer;
begin
h:= x mod p;
end;
我们注意到,插入和查找首先都需要对这个元素定位,即如果这个元素若存在,它应该存储在什么位置,因此
加入一个定位的函数 locate
function locate(x:longint):integer;
var orig,i:integer;
begin
orig:=h(x);
i:=0;
while (i<S) and (A[(orig+i) mod S]<>x) and (A[(orig+i) mod S]<>empty) do
inc(i);
//当这个循环停下来时,要么找到一个空的存储单元,要么找到这个元素存储的单元,要么表已经满了
locate:=(orig+i) mod S;
end;
插入元素
procedure insert(x:longint);
var posi:integer;
begin
posi:=locate(x); //定位函数的返回值
if A[posi]=empty then A[posi]:=x
else error; //error 即为发生了错误,当然这是可以避免的
end;
查找元素是否已经在表中
procedure member(x:longint):boolean;
var posi:integer;
begin
posi:=locate(x);
if A[posi]=x then member:=true
else member:=false;
end;
这些就是建立在哈希表上的常用基本运算。
3. 效率对比
3.1 简单的例子与实验
下面是一个比较简单的例子:
集合 ( Subset )
问题描述:
给定两个集合 A、B,集合内的任一元素 x 满足 1 ≤ x ≤ ,并且每个集合的元素个数不大于 个。我们
希望求出 A、B 之间的关系。只需确定在 B 中但是不在 A 中的元素的个数即可。(这个题目是根据 OIBH NOIP 2002
模拟赛 # 1 的第一题改编的。)
分析:我们先不管 A 与 B 的具体关系如何,注意到这个问题的本质就是对于给定的集合 A ,确定 B 中的元素
是否在 A 中。所以,我们使用哈希表来处理。至于哈希函数,只要按照除余法就行了,由于故意扩大了原题的数据
规模, H(x) = x mod 15889;
当然本题可以利用别的方法解决,所以选取了速度最快的快速排序+二分查找,让这两种方法作效率对比。
我们假定 |A|=|B| ,对于随机生成的数据,计算程序重复运行 50 次所用时间。
对比表格如下:
哈希表(sec)
快速排序+二分查找(sec)
复杂度
O(N)
(只有忽略了冲突才是这个结果。当然实际情况会比这个
大,但是重复的几率与哈希函数有关,不容易估计)
O(N log N+ N) = O(N log N)
测试数据规模
——
——
500
0.957
0.578
1000
1.101
0.825
2500
1.476
1.565
5000
2.145
2.820
7500
2.905
4.203
10000
3.740
5.579
13500
7.775
7.753
15000
27.550
8.673
对于数据的说明:在 Celeron566 下用 TP 测试,为了使时间的差距明显,让程序重复运了行 50 次。同时哈希
表中的 P= 15889 ,下标范围 0..15888 。由于快速排序不稳定,因此使用了随机数据。
3.2 对试验结果的分析:
注意到两个程序的用时并不像我们期望的那样,总是哈希表快。设哈希表的大小为 P.
首先,当规模比较小的时候(大约为 a< 10% * P,这个数据仅仅是通过若干数据估记出来的,没有严格证明,
下同),第二种方法比哈希表快。这是由于,虽然每次计算哈希函数用 O(1) 的时间,但是这个系数比较大。例如
这道题的 H(x)=x mod 15889 ,通过与做同样次数的加法相比较,测试发现系数 > 12 ,因为 mod 运算本身与快速
排序的比较大小和交换元素运算相比,比较费时间。所以规模小的时候,O(N)(忽略冲突)的算法反而不如 O(NlogN)。
这一点在更复杂的哈希函数上会体现的更明显,因为更复杂的函数系数会更大。
其次,当规模稍大 (大约为 15%*P < a < 85%*P) 的时候,很明显哈希表的效率高。这是因为冲突的次数较
少。
再次,当规模再大 (大约为 90%*P < a < P )的时候,哈希表的效率大幅下降。这是因为冲突的次数大大提
高了,为了解决冲突,程序不得不遍历一段都存储了元素的数组空间来寻找空位置。用白箱测试的方法统计,当规
模为 13500 的时候,为了找空位置,线性重新散列平均做了 150000 次运算;而当规模为 15000 的时候,平均竟然
高达 2000000 次运算,某些数据甚至能达到 4265833 次。显然浪费这么多次运算来解决冲突是不合算的,解决这个
问题可以扩大表的规模,或者使用"开散列"(尽管它是动态数据结构)。然而需要指出的是,冲突是不可避免的。
初步结论:
当数据规模接近哈希表上界或者下界的时候,哈希表完全不能够体现高效的特点,甚至还不如一般算法。但是
如果规模在中央,它高效的特点可以充分体现。我们可以从图像直观的观察到这一点。
试验表明当元素充满哈希表的 90% 的时候,效率就已经开始明显下降。这就给了我们提示:如果确定使用哈希
表,应该尽量使数组开大(由于竞赛中可利用内存越来越多,大数组通常不是问题,当然也有少数情况例外),但
对最太大的数组进行操作也比较费时间,需要找到一个平衡点。通常使它的容量至少是题目最大需求的 120% ,效
果比较好(这个仅仅是经验,没有严格证明)。
4. 应用举例
4.1 应用的简单原则
什么时候适合应用哈希表呢?如果发现解决这个问题时经常要询问:"某个元素是否在已知集合中?",也就是
需要高效的数据存储和查找,则使用哈希表是最好不过的了!那么,在应用哈希表的过程中,值得注意的是什么呢?
哈希函数的设计很重要。一个不好的哈希函数,就是指造成很多冲突的情况,从前面的例子已经可以看出来,
解决冲突会浪费掉大量时间,因此我们的目标就是尽力避免冲突。前面提到,在使用"除余法"的时候,h(k)=k mod p ,
p 最好是一个大素数。这就是为了尽力避免冲突。为什么呢?假设 p=1000 ,则哈希函数分类的标准实际上就变成
了按照末三位数分类,这样最多 1000 类,冲突会很多。一般地说,如果 p 的约数越多,那么冲突的几率就越大。
简单的证明:假设 p 是一个有较多约数的数,同时在数据中存在 q 满足 gcd(p,q)=d >1 ,即有 p=a*d , q=b*d,
则有 q mod p= q - p* [q div p] =q - p*[b div a] . ① 其中 [b div a ] 的取值范围是不会超过 [0,b] 的正
整数。也就是说, [b div a] 的值只有 b+1 种可能,而 p 是一个预先确定的数。因此 ① 式的值就只有 b+1 种
可能了。这样,虽然 mod 运算之后的余数仍然在 [0,p-1] 内,但是它的取值仅限于 ① 可能取到的那些值。也就
是说余数的分布变得不均匀了。容易看出, p 的约数越多,发生这种余数分布不均匀的情况就越频繁,冲突的几率
越高。而素数的约数是最少的,因此我们选用大素数。记住"素数是我们的得力助手"。
另一方面,一味的追求低冲突率也不好。理论上,是可以设计出一个几乎完美,几乎没有冲突的函数的。然而,
这样做显然不值得,因为这样的函数设计很浪费时间而且编码一定很复杂,与其花费这么大的精力去设计函数,还
不如用一个虽然冲突多一些但是编码简单的函数。因此,函数还需要易于编码,即易于实现。
综上所述,设计一个好的哈希函数是很关键的。而"好"的标准,就是较低的冲突率和易于实现。
另外,使用哈希表并不是记住了前面的基本操作就能以不变应万变的。有的时候,需要按照题目的要求对哈希
表的结构作一些改进。往往一些简单的改进就可以带来巨大的方便。
这些只是一般原则,真正遇到试题的时候实际情况千变万化,需要具体问题具体分析才行。下面,我们看几个
例子,看看这些原则是如何体现的。
4.2 有关字符串的例子
我们经常会遇到处理字符串的问题,下面我们来看这个例子:
找名字
问题描述:
给定一个全部由字符串组成的字典,字符串全部由大写字母构成。其中为每个字符串编写密码,编写的方式是
对于 n 位字符串,给定一个 n 位数,大写字母与数字的对应方式按照电话键盘的方式:
剩余21页未读,继续阅读
资源评论
- lcltmac2014-07-28很好的资源。
zl200972172
- 粉丝: 0
- 资源: 9
上传资源 快速赚钱
- 我的内容管理 展开
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助
安全验证
文档复制为VIP权益,开通VIP直接复制
信息提交成功