哈希表查找
哈希表可以通过关键字直接找到数据的存储位置,不需要进行任何的比较,也就是说,哈希表建立了关键字和存储地址之间的一种直接映射关系。其查找的效率相较于前面所学习的查找算法是更高的。
一、认识哈希表
在初中的数学课本中学习过函数的相关知识,给定一个 x
,通过一个数学公式,只需要将 x
的值带入公式就可以求出一个新的值 y
。
哈希表的建立同函数类似,把函数中的 x
用查找记录时使用的关键字来代替,然后将关键字的值带入一个精心设计的公式中,就可以求出一个值,用这个值来表示记录存储的哈希地址。即:
数据的哈希地址=f(关键字的值)
哈希地址只是表示在查找表中的存储位置,而不是实际的物理存储位置。
f()
是一个函数,通过这个函数可以快速求出该关键字对应的的数据的哈希地址,称之为“哈希函数”。
例如,这里有一个电话簿(查找表),电话簿中有 4 个人的联系方式:
姓名 | 电话 |
---|---|
张三 | 13912345678 |
李四 | 15823457890 |
王五 | 13409872338 |
赵六 | 13805834722 |
假如想查找李四的电话号码,对于一般的查找方式最先想到的是从头遍历,一一比较。而如果将电话簿构建成一张哈希表,可以直接通过名字“李四”直接找到电话号码在表中的位置。
在构建哈希表时,最重要的是哈希函数的设计。例如设计电话簿案例中的哈希函数为:每个名字的姓的首字母的 ASCII 值即为对应的电话号码的存储位置。这时会发现,张三和赵六两个关键字的姓的首字母都是 Z
,最终求出的电话号码的存储位置相同,这种现象称为冲突。在设计哈希函数时,要尽量地避免冲突现象的发生。
对于哈希表而言,冲突只能尽可能地少,无法完全避免。
二、哈希函数的构造
常用的哈希函数的构造方法有 6 种:直接定址法、数字分析法、平方取中法、折叠法、除留余数法和随机数法。
1、直接定址法(重点)
其哈希函数为一次函数,即以下两种形式:H(key)= key
或者 H(key)=a * key + b
其中 H(key
)表示关键字为 key
对应的哈希地址,a
和b
都为常数。
例如有一个从 1 岁到 100 岁的人口数字统计表,如下所示:
假设其哈希函数为第一种形式,其关键字的值表示最终的存储位置。若需要查找年龄为 25 岁的人口数量,将年龄 25 带入哈希函数中,直接求得其对应的哈希地址为 25(求得的哈希地址表示该记录的位置在查找表的第 25 位)。
2、数字分析法(重点)
如果关键字由多位字符或者数字组成,就可以考虑抽取其中的 2 位或者多位作为该关键字对应的哈希地址,在取法上尽量选择变化较多的位,避免冲突发生。适合于已知的关键字集合。
例如下图中列举的是一部分关键字,每个关键字都是有 8 位十进制数组成
通过分析关键字的构成,很明显可以看到关键字的第 1 位和第 2 位都是固定不变的,而第 3 位不是数字 3 就是 4,最后一位只可能取 2、7 和 5,只有中间的 4 位其取值近似随机,所以为了避免冲突,可以从 4 位中任意选取 2 位作为其哈希地址。
3、平方取中法
将关键字做平方操作,取中间得几位作为哈希地址。此方法也是比较常用的构造哈希函数的方法。
例如关键字序列为{421
,423
,436
},对各个关键字进行平方后的结果为{177241,178929,190096},则可以取中间的两位{72
,89
,00
}作为其哈希地址。
4、折叠法
将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。此方法适合关键字位数较多的情况。
例如,在图书馆中图书都是以一个 10 位的十进制数字为关键字进行编号的,若对其查找表建立哈希表时,就可以使用折叠法。
若某书的编号为:0-442-20586-4
,分割方式如下图所示,在对其进行折叠时有两种方式:一种是移位折叠,另一种是间界折叠:
- 移位折叠是将分割后的每一小部分,按照其最低位进行对齐,然后相加,如图(a);
- 间界折叠是从一端向另一端沿分割线来回折叠,如图(b)。
5、除留余数法(重点)
若已知整个哈希表的最大长度 m
,可以取一个不大于 m
的数 p
,然后对该关键字 key
做取余运算,即:H(key)= key % p (p≤m)
。适用于哈希表大小已知。
在此方法中,对于 p 的取值非常重要,由经验得知 p 可以为不大于 m 的质数或者不包含小于 20 的质因数的合数。
6、随机数法
取关键字的一个随机函数值作为它的哈希地址,即:H(key)=random(key)
,此方法适用于关键字长度不等的情况。
注意:这里的随机函数其实是伪随机函数,随机函数是即使每次给定的
key
相同,但是H(key)
都是不同;而伪随机函数正好相反,每个key
都对应的是固定的H(key)
。
我们在选择的时候,需要根据实际的查找表的情况采取适当的方法。通常考虑的因素有以下几方面:
- 关键字的长度。如果长度不等,就选用随机数法。如果关键字位数较多,就选用折叠法或者数字分析法;反之如果位数较短,可以考虑平方取中法;
- 哈希表的大小。如果大小已知,可以选用除留余数法;
- 关键字的分布情况;
- 查找表的查找频率;
- 计算哈希函数所需的时间(包括硬件指令的因素)
三、处理冲突的方法
对于哈希表的建立,需要选取合适的哈希函数,但是对于无法避免的冲突,需要采取适当的措施去处理。
通常用的处理冲突的方法有以下几种:
1、开放定址法
开放定址法是在出现哈希冲突时在哈希表中找一个新的空闲位置存放元素。
(1)线性探测法
简单来说,当冲突发生时,顺序查看表中下一个单元(当探测到表尾地址m-1时,下一个探测地址是表首地址0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表。
优点:解决冲突简单
缺点:容易产生堆积问题
(2)平方探测法
简单来说,设发生冲突的地址为d,平方探测法得到的新的地址序列为d+12,d-12,d+22,d-22......
优点:可以避免出现“堆积”问题
缺点:不能探测到散列表上的所有单元,但至少能探测到一半单元。
2、链地址法
将所有产生冲突的关键字所对应的数据全部存储在同一个线性链表中。拉链法适用于经常进行插入和删除的情况。例如有一组关键字为{19
,14
,23
,01
,68
,20
,84
,27
,55
,11
,10
,79
},其哈希函数为:H(key)=key MOD 13
,使用链地址法所构建的哈希表如下所示:
优点:提高查找速率
缺点:需要额外空间
3、再哈希法
当通过哈希函数求得的哈希地址同其他关键字产生冲突时,使用另一个哈希函数计算,直到冲突不再发生。
四、哈希表查找的性能
在构造哈希表的过程中,由于冲突的产生,使得哈希表的查找算法仍然会涉及到比较的过程,因此对于哈希表的查找效率仍需以平均查找长度来衡量。
在哈希表的查找过程中需和给定值进行比较的关键字的个数取决于以下 3 个因素:
- 哈希函数:哈希函数的“好坏”取决于影响出现冲突的频繁程度。但是一般情况下,哈希函数相比于后两种的影响,可以忽略不计。
- 处理冲突的方式:对于同一组关键字,设定相同的哈希函数,使用不同的处理冲突的方式得到的哈希表是不同的,表的平均查找长度也不同。
- 哈希表的装填因子:在一般情况下,当处理冲突的方式相同的情况下,其平均查找长度取决于哈希表的装满程度:装的越满,插入数据时越有可能发生冲突;反之则越小。
- 设填入表的记录个个数为
m
,散列表的长度为n
,则:α = m / n
装填因子=哈希表中数据的个数/哈希表的长度,用字符
α
表示(是数学符号,而不是字符 a)。装填因子越小,表示哈希表中空闲的位置就越多。
经过计算,在假设查找表中的所有数据的查找概率相等的情况下,对于表长为 m,数据个数为 n 的哈希表:
- 其查找成功的平均查找长度约为:
-1/α * ln?(1-α)
- 其查找不成功的平均查找长度约为:
1/(1-α)
通过公式可以看到,哈希表的查找效率只同装填因子有关,而同哈希表中的数据的个数无关,所以在选用哈希表做查找操作时,选择一个合适的装填因子是非常有必要的。
1、Java代码实现哈希查找
//学习于CSDN作者-李闪磊 public class HashSearch { //定义基本结构: int[] elem; //散列表数据存储数组 public int count; //散列表实际存储数据量 private int maxSize = 20; //散列表的最大容量 public final int NULLKEY = -32769; //散列表初始值 public final int SUCCESS = 1; public final int UNSUCCESS = 0; //对散列表进行初始化: public HashSearch() { this.elem = new int[maxSize]; this.initHashSearch(); } public HashSearch(int maxsize) { this.maxSize = maxsize; this.elem = new int[maxSize]; this.initHashSearch(); } public void initHashSearch() { for (int i = 0; i < maxSize; i++) { this.elem[i]= NULLKEY; } } //散列函数: /** * 散列函数 * 保留余数法 * @param key * @return */ public int Hash(int key) { return key % maxSize; } //散列表的插入操作: public void insertHash(int key) { int addr = Hash(key); //求散列地址 while(this.elem[addr] != NULLKEY) { addr = Hash(addr + 1); //开放定址法的线性探测 } this.elem[addr] = key; ++count; } //查找操作: public int searchHash(int key) { int addr = Hash(key); while(this.elem[addr] != key) { addr = Hash(addr + 1); //开放定址法的线性探测 if(this.elem[addr] == NULLKEY || addr == Hash(key)) { //如果循环回到原点 return UNSUCCESS; //说明关键字不存在 } } return SUCCESS; } //测试代码: public static void main(String[] args) { HashSearch h = new HashSearch(); h.insertHash(5); h.insertHash(4); h.insertHash(3); h.insertHash(6); h.insertHash(8); h.insertHash(9); h.insertHash(1); h.insertHash(7); System.out.println("插入数据数量:" + h.count); System.out.println("是否存在关键字为0的值:" + h.searchHash(0)); System.out.println("是否存在关键字为9的值:" + h.searchHash(9)); } }
2、一道例题
3、对比
下面一章学习是排序了,很早以前就有所学习,之前转载了一篇好文,可以看看十大经典排序算法