数据结构---散列表查找(哈希表)概述和简单实现(Java)
散列表查找定义
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,是的每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定值的key的对应f(key)。
我们把这种对应关系f称为散列函数,又称哈希(Hash)函数,按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间成为散列表或哈希表。关键字对应的记录存储位置我们成为散列地址。
查找时的步骤:
- 在存储时,通过散列函数计算记录的散列地址,并按散列地址存储该记录。
- 当查找记录时,通过同样的散列函数计算记录的散列地址,按散列地址访问该记录
所以说,散列技术既是一种存储方法,又是一种查找方法,散列技术的记录之间不存在什么逻辑关系,它只与关键字有关。散列技术最适合的求解问题就是查找和给定值相等的记录。
散列冲突:当两个关键字key1key_1key1!=key2key_2key2,但是却有f(key1key_1key1) =f(key2key_2key2),这种现象我们叫做散列冲突,并把key1key_1key1和key2key_2key2称为这个散列函数的同义词。
散列函数的构造方法
散列函数的构造方法遵循两个原则:
- 计算简单
- 散列地址分布均匀
直接定址法
比如我们现在要统计80后出生年份的人口数,那么我们对出生年份这个关键字可以用年份减去1980来作为地址。此时f(key)=key-1980。
地址 | 出生年份 | 人数 |
---|---|---|
0 | 1980 | 1500万 |
01 | 1981 | 1600万 |
02 | 1982 | 1300万 |
… | … | … |
2000 | 2000 | 800万 |
直接定址法就去取关键字的某个线性函数值为散列地址
f(key)=a * key +b
这样的散列函数的优点就是简单均匀,也不会产生冲突,但问题是需要事先知道关键字的分布情况,适合查找表较小且连续的情况。所以这个方法并不常用。
数字分析法
如果我们的关键字是位数较多的数字,比如用11位的手机号"130xxxx1234",其中前三位是接入号,中间四位是HLR识别号,后四位才是真正的用户号,那么我们选择后四位作为散列地址就是不错的选择如果害怕存在冲突现象,我们还可以进行对数字的翻转,右环位移等方法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布均匀,可以考虑使用这个方法。
平方取中法
假设关键字是1234,那么它的平方就是1522756,在抽取中间的三位就是227,用作散列地址。
平方取中法适合不知道关键字的分布,而位数又不是很大的情况
折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够可以短些),然后将这几部分进行叠加求和,并按散列表表长,取后几位作为散列地址。
比如我们的关键字是9876543210,散列表表长为3位,我们将它分为4组,987|654|321|0,然后将他们叠加求和为987+654+321+0=1962,在求后面的三位为散列地址962。
折叠法实现不需要知道关键字的分布,适合关键字较多的情况。
除留取余法
此方式为最常用的构造函数方法。对于散列表长为m的散列函数公式为:
f(key) = key mod p (p <= m)
mod是取模(求余数)的意思,该方法的关键就是在于选取合适的p,p如果选的不好,就可能容易出现同义词。
一般来说,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
随机数法
选择一个随机数,取关键字的随机函数值为它的散列地址。也就是f(key)=random(key)。这里random是随机函数,如果关键字长度不等的情况下,采用这个方法构造散列函数是比较合适的。
对于字符串的处理,可以将其转化为ASCII或Unicode码。
总结
在选取散列函数时应从一下因素考虑:
- 计算散列地址所需的时间
- 关键字的长度
- 散列表的大小
- 关键字的分布情况
- 记录查找的频率
综合这些因素,才能决策选择哪种散列函数更合适
处理散列冲突的方法
开放定址法
开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能寻找到,并将记录记下。常用的开放定址法有:线性探查法、二次探查法、随机探测法
线性探测法
设映射函数为f,表的规模为m,被映射的关键字是key。如果在表中散列位置f(key)上发生冲突,那么线性探查法依次检查位置(f(key) + did_idi)mod m, i=1,2,…,直到某个(f(key) + did_idi)是空位置,或者(f(key) + did_idi)mod m = f(key)结束。
fif_ifi(key) = (f(key) + did_idi) MOD m (did_idi=1, 2, 3, 4, 5, …, m-1)
线性探测法存在一个问题:考虑最坏情况下,所有的存储值都同一位置存在冲突,每次寻找一个新的位置存储数据,第一次冲突寻找1次,第二次冲突2次,直到第N-1次冲突,需要寻找N-1次。像这种本来不是同义词却需要争夺一个地址的情况,我们称为堆积。堆积的出现,使得我们需要不断的处理冲突,这种堆积效应使得插入和查找的复杂度都变为O(N)。
二次探测法
加入一个散列表最后的key=34,f(key)=10,与它之前的22所在位置冲突,但是22后面没有空位置了,反而它的前面有一个空位置,尽管我们可以不断的求余数后得到结果,但是效率很差,因此我们可以改进did_idi=121^212,−12{-1}^2−12,222^222,−22-2^2−22,…,q2q^2q2,−q2-q^2−q2,(q<=m/2)
这样就等于可以双向寻找可能的空位置,另外增加平方运算的目的是为了不让关键字都聚集在某个快区域,我们称这种方法叫做二次探测法:
设散列函数为f,表的规模为m,要散列的关键字为key。那么,如果在散列位置f(key)发生冲突,二次探查法依次检查位置(f(key) + i2i^2i2),直到某个位置是个空位置,或者已经检查过的位置。
fif_ifi(key) = (f(key) + did_idi) MOD m (did_idi=121^212,−12{-1}^2−12,222^222,−22-2^2−22,…,q2q^2q2,−q2-q^2−q2,(q<=m/2))
相对线性探查法,二次探查确实可以一定程度避免堆积。但二次探查法最坏情况下,即所有关键字在同一个位置冲突下,数组的利用率为1/2。可以证明,对于任意素数N,一旦一个位置被检查两次,那么之后的所有位置都是被已检查过的位置。
//设在i和j结束于相同位置
(h+ i2) mod N = (h+j2) mod N
→ (i+j)(i-j) mod N = 0
//因为N是素数,它必须整除因子(i+j)或(i-j),只有做了N次探查,N才能整除(i-j);同时,使得N整除(i+j)的最小(i+j)为N。
→ i+j = N → j = N - i
//故而不同的探查位置数只能是N/2。
最坏情况的搜索和插入运行时间依旧是O(N)。
随机探测法
在冲突时,对于位移量did_idi采用随机函数计算得到的,我们称之为随机探测法
fif_ifi(key)= (f(key) + did_idi) MOD m (did_idi是一个随机数列)
再散列函数法
对于散列表,可以准备多个散列函数
fif_ifi(key) = RHiRH_iRHi(key) (i=1,2,3,…,k)
RHiRH_iRHi就是不同的散列函数,每当发生散列地址冲突时,就换一个散列函数计算,这种方法能够使得关键字不产生聚集,但是相应的也增加了计算时间。
链地址法(封闭寻址法)
将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词字表,在散列表中只存储所有同义词字表的头指针。在java中java.util.HashMap就采用这样的设计。java中HashMap是一种字典结构,实现了散列表的功能,存储(key,value)键值对,至少支持get(key)、put(key,value)、delete(key)方法。广义上来说,列表和二叉查找树都是字典。
同开放寻址法,最坏的插入和搜索的时间复杂度都是O(n),当然如果是对关键字完美散列的散列函数,时间复杂度都是O(1)。
公共溢出法
为所有冲突的关键字建立一个公共的溢出区来进行存放。在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行对比,如果相等则查找成功;如果不懂,则到溢出表中进行顺序查找,在冲突的数据很少情况下,公共出去的结构对查找性能来说还是很高的。
散列表的查找实现
定义基本结构:
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 HashTable() {
this.elem = new int[maxSize];
this.initHashTable();
}
public HashTable(int maxsize) {
this.maxSize = maxsize;
this.elem = new int[maxSize];
this.initHashTable();
}
public void initHashTable() {
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) {
HashTable h = new HashTable();
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));
}
结果:
插入数据数量:8
是否存在关键字为0的值:0
是否存在关键字为9的值:1
散列表查找性能分析
理想情况下散列查找的效率最高为O(1),当然只是理想情况下。。。
散列查找的平均查找长度取决于下面的因素:
- 散列函数是否均匀
散列函数的好坏直接影响着出现冲突的频繁程度,但是由于不同的散列函数对同一组随机的关键字,产生冲突的可能性是相同的。所以一般不考虑它对平均查找长度的影响。 - 处理冲突的方法
一般来说,线性探测处理冲突可能会产生堆积,显然没有二次寻址法好,而链地址法处理冲突不会产生任何堆积,因而具有更佳的平均查找性能。 - 散列表的填装因子
设填装因子为a,填入表的记录个个数为m,散列表的长度为n,则:
a = m / n
填装因子标志着散列表的装满的程度,当填入表的记录越多,a越大,产生冲突的可能性就越大。无论记录个数m有多大,我们总可以选择一个合适的填装因子以便将平均查找长度限定在一个范围之内,通常我们都是将散列表的空间设置的比查找集合大,虽然浪费一定的空间,但是查找效率大大提升。
数据结构---散列表查找(哈希表)概述和简单实现(Java)的更多相关文章
- 【Java】 大话数据结构(13) 查找算法(4) (散列表(哈希表))
本文根据<大话数据结构>一书,实现了Java版的一个简单的散列表(哈希表). 基本概念 对关键字key,将其值存放在f(key)的存储位置上.由此,在查找时不需比较,只需计算出f(key) ...
- 数据结构(四十二)散列表查找(Hash Table)
一.散列表查找的基础知识 1.散列表查找的定义 散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key).查找时,根据这个确定的对应关系找到 ...
- 哈希表(散列表),Hash表漫谈
1.序 该篇分别讲了散列表的引出.散列函数的设计.处理冲突的方法.并给出一段简单的示例代码. 2.散列表的引出 给定一个关键字集合U={0,1......m-1},总共有不大于m个元素.如果m不是很大 ...
- 【PHP数据结构】散列表查找
上篇文章的查找是不是有意犹未尽的感觉呢?因为我们是真真正正地接触到了时间复杂度的优化.从线性查找的 O(n) 直接优化到了折半查找的 O(logN) ,绝对是一个质的飞跃.但是,我们的折半查找最核心的 ...
- 哈希表查找(散列表查找) c++实现HashMap
算法思想: 哈希表 什么是哈希表 在前面讨论的各种结构(线性表.树等)中,记录在结构中的相对位置是随机的,和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和关键字的比较.这一类 ...
- 自己动手实现java数据结构(五)哈希表
1.哈希表介绍 前面我们已经介绍了许多类型的数据结构.在想要查询容器内特定元素时,有序向量使得我们能使用二分查找法进行精确的查询((O(logN)对数复杂度,很高效). 可人类总是不知满足,依然在寻求 ...
- Java数据结构和算法之哈希表
五.哈希表 一般的线性表.树中,记录在结构中的相对位置是随机的即和记录的关键字之间不存在确定的关系,在结构中查找记录时需进行一系列和关键字的比较.这一类查找方法建立在“比较”的基础上,查找的效率与比较 ...
- Python数据结构——散列表
散列表的实现常常叫做散列(hashing).散列仅支持INSERT,SEARCH和DELETE操作,都是在常数平均时间执行的.需要元素间任何排序信息的操作将不会得到有效的支持. 散列表是普通数组概念的 ...
- C# 哈希表HashTable的简单使用
本人C#程序菜鸟级别的存在,写博客一方面是为了知识的共享,另一方面也是为了督促自己:大神,可以忽略这篇文文的.废话到此...... 哈希表是可以直接进行访问的数据结构,在形式上是类似字典的.不同的是, ...
随机推荐
- 让IIS 7 如同IIS 8 第一次请求不变慢
当我们把网站部署在IIS7或IIS6S的时候,每当IIS或是Application Pool重启后,第一次请求网站反应总是很慢,原因大家都知道(不知道可以参考这个动画说明ASP.NET网页第一个Req ...
- 2018.09.15 bzoj1977:次小生成树 Tree(次小生成树+树剖)
传送门 一道比较综合的好题. 由于是求严格的次小生成树. 我们需要维护一条路径上的最小值和次小值. 其中最小值和次小值不能相同. 由于不喜欢倍增我选择了用树链剖分维护. 代码: #include< ...
- 2018.08.27 rollcall(非旋treap)
描述 初始有一个空集,依次插入N个数Ai.有M次询问Bj,表示询问第Bj个数加入集合后的排名为j的数是多少 输入 第一行是两个整数N,M 接下来一行有N个整数,Ai 接下来一行有M个整数Bj,保证数据 ...
- PHP二个高精确度数字相加减
1.相加 string bcadd(string left operand, string right operand, int [scale]); 2.相减 string bcsub(string ...
- PHP compact函数
$firstname = "Peter"; $lastname = "Griffin"; $age = "41"; $data = comp ...
- EXCEL 单元格引用问题
=(SUM(INDIRECT("'2.5酒店预订收入'!"&"J"&MATCH(C21,'2.5酒店预订收入'!B:B,0)&" ...
- An existing resource has been found at location D:\Tomcat 7\apache-tomcat-7.0.55\webapps。。。
这个错误是说你的资源丢失,就是说tomcat无法解析你的.class文件,需要自己重新配置一下. 解决方法: 右击项目名 ---> 点击properties --> 在搜索栏里 输入 WE ...
- 获取iOS 设备上崩溃日志 (Crash Log)的方法
1. iTunes同步获取 大部分用户会使用iTunes软件来管理iPhone,这样同步的Crash日志就会同步到电脑上,我们需要在特定的路径里面查找 Mac OS X:~/Library/Logs/ ...
- 转:解决windows下eclipse中android项目关联android library project失败问题
近日,在做一个人人的第三方小项目.打算直接使用renren 的sdk 进行开发.因为renren的sdk是以android library project 形式发布的(关于这种project的内容可以 ...
- HDU1241 Oil Deposits 2016-07-24 13:38 66人阅读 评论(0) 收藏
Oil Deposits Problem Description The GeoSurvComp geologic survey company is responsible for detectin ...