数据结构---散列表查找(哈希表)概述和简单实现(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#程序菜鸟级别的存在,写博客一方面是为了知识的共享,另一方面也是为了督促自己:大神,可以忽略这篇文文的.废话到此...... 哈希表是可以直接进行访问的数据结构,在形式上是类似字典的.不同的是, ...
随机推荐
- nodejs的优点
nodejs主要用于搭建高性能的web服务器,优点如下: 可以解决高并发,它是单线程,当访问量很多时,将访问者分配到不同的内存中,不同的内存区做不同的事,以快速解决这个线程.就像医院的分科室看病人.效 ...
- js 验证input 输入框
<h1>js验证输入框内容</h1><br /><br /> 只能输入英文<input type="text" onkeyup ...
- UVa 10970 Big Chocolate (想一下就AC了)
题意:给你一个m*n的巧克力,让人把它切成1*1的,但是每次只能切一下,问要切多少刀. 析:简单啊,我就不明白了 怎么那么多人TLE了,不会当DP做了吧,其实不用的. 假设有一个1*m的巧克力,很明显 ...
- DIV+CSS 中的 overflow:hidden
overflow:hidden这个CSS样式是大家常用到的CSS样式,但是大多数人对这个样式的理解仅仅局限于隐藏溢出,而对于清除浮动这个含义不是很了解. 一提到清除浮动,我们就会想到另外一个CSS样式 ...
- 测试setsockopt设置超时是否生效代码
// 测试setsockopt设置超时是否生效代码 #include <arpa/inet.h> #include <netinet/in.h> #include <st ...
- faceswap requirements
tqdm psutil pathlib==1.0.1 scandir==1.7 opencv-python scikit-image scikit-learn matplotlib==2.2.2 ff ...
- bootstrap 问题
less; sass: css预处理:可以直接使用.css,也可以修改.less,生成定制化的css CDN: 服务,使用这个效果会更好.theme一般不引入,jquery一般在js之前引入. 使用b ...
- Oracle定义DES加密解密及MD5加密函数
http://blog.csdn.net/xdweleven/article/details/38319351 (1)DES加密函数create or replace functionencryp ...
- Python学习-20.Python的Urllib模块
除了 Http 模块可以模拟 Http 请求外,使用 Urllib 模块也是可以模拟 Http 请求的,只不过功能相对弱一点. import urllib.request opener = urlli ...
- 高并发Web
hadoop适合处理分布式集群系统,本身是支持高速并发海量数据的写入和读取的.解决大量用户并发访问的方案有很多,给你个千万pv的参考方案:1)架构中直接引入软件名称的模块,是个人推荐使用的,如Hapr ...