Map集合、散列表
一、Map
1.1、为什么需要map
前面我们学习的Collection叫做集合,它可以快速查找现有的元素。而Map在《Core Java》中称之为–>映射,映射的模型图是这样的:
那为什么我们需要这种数据存储结构呢???举个例子
- 作为学生来说,我们是根据学号来区分不同的学生。只要我们知道学号,就可以获取对应的学生信息。这就是Map映射的作用!
生活中还有很多这样的例子:只要你掏出身份证(key),那就可以证明是你自己(value)
1.2、Map与Collection的区别
Map集合的 特点:将键映射到值的对象 ,一个映射不能包含重复的键,一个键最多只能映射一个值(注意最后的一句话:一个键最多只能映射一个值,说明就是一个键能映射一个值或者是0个值)
- Map:集合中存储的元素是成对出现的,map的键是唯一的,值是可以重复的。
- collection中存储的元素是单独出现的,Collection的儿子set是唯一的,List是可以重复的
要点:map集合的数据结构针对键有效,跟值无关
Collection集合的数据结构针对元素有效
1.3、Map的大家庭
1.4、简单的介绍一下Map的常用的功能
二、散列表介绍
无论是Set还是Map,我们会发现都会有对应的–>**Hash**Set,**Hash**Map
首先我们也先得回顾一下数据和链表:
- 链表和数组都可以按照人们的意愿来排列元素的次序,他们可以说是有序的(存储的顺序和取出的顺序是一致的)
- 但同时,这会带来缺点:想要获取某个元素,就要访问所有的元素,直到找到为止。
- 这会让我们消耗很多的时间在里边,遍历访问元素~
而还有另外的一些存储结构:不在意元素的顺序,能够快速的查找元素的数据
- 其中就有一种非常常见的:散列表
2.1、散列表工作原理
我们先看看hash是什么?下面是一段摘自维基百科的解释
散列(hashing)是电脑科学中一种对资料的处理方法,通过某种特定的函数/算法(称为散列函数/算法)将要检索的项与用来检索的索引(称为散列,或者散列值)关联起来,生成一种便于搜索的数据结构(称为散列表)。也译为散列。旧译哈希(误以为是人名而采用了音译)。它也常用作一种资讯安全的实作方法,由一串资料中经过散列算法(Hashing algorithms)计算出来的资料指纹(data fingerprint),经常用来识别档案与资料是否有被窜改,以保证档案与资料确实是由原创者所提供。 —-Wikipedia
2.2、哈希函数
所有的哈希函数都具有如下一个基本特性:
如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。
2.3、hash冲突
在理想的情况下,每一个关键字,通过散列函数(哈希函数)计算出来的地址都是一样的,可现实中,这只是一个理想的情况。我们时常会碰到两个关键字key1不等于key2,但是却有f(key1)=f(key2),这种现象称为冲突(collision),并把key1和key2称为这个散列函数的同义词;
综上所述,根据散列函数f(k)
和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为散列表,这一映射过程称为散列造表或散列,所得的存储位置称散列地址。
若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function
),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。
2.4、哈希函数构造方法
上面我们已经引出了并解释了Hash函数 。实际工作中,需要视不同的情况采用不同的Hash函数 ,通常要考虑的因素有:
- Hash函数 执行的时间;
- 关键字 的长度;
- Hash表 的大小;
- 关键字 的分布情况;
- 记录 的查找频率;
1、直接寻址法:
取关键字的某个线性函数值为Hash地址f(key)=a*key+b(a,b常数) 。
特点:由于直接地址法相当于有多少个关键字就必须有多少个相应地址去对应,所以不会产生冲突,也正因为此,所以实际中很少使用这种构造方法。
2、数字分析法:
首先分析待存的一组关键字 ,比如是一个班级学生的出生年月日 ,我们发现他们的出生年 大体相同,那么我们肯定不能用他们的年 来作为存储地址 ,这样出现冲突 的几率很大;但是,我们发现月日 的具体数字差别很大,如果我们用月日 来作为Hash地址 ,则会明显降低冲突几率。因此,数字分析法就是找出关键字 的规律,尽可能用差异数据来构造Hash地址 ;
特点:需要提前知道所有可能的关键字的分布,才能分析运用此种方法,所以不太常用。
3、平方取中法:
先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。
例:我们把英文字母在字母表中的位置序号作为该英文字母的内部编码。例如K的内部编码为11,E的内部编码为05,Y的内部编码为25,A的内部编码为01, B的内部编码为02。由此组成关键字“KEYA”的内部代码为11052501,同理我们可以得到关键字“KYAB”、“AKEY”、“BKEY”的内部编码。之后对关键字进行平方运算后,取出第7到第9位作为该关键字哈希地址,如下图所示:
关键字 | 内部编码 | 内部编码的平方值 | H(k)关键字的哈希地址 |
---|---|---|---|
KEYA | 11050201 | 122157778355001 | 778 |
KYAB | 11250102 | 126564795010404 | 795 |
AKEY | 01110525 | 001233265775625 | 265 |
BKEY | 02110525 | 004454315775625 | 315 |
特点:适合不知道关键字的分布,而且位数 又不是很大的情况
4、折叠法:
将关键字分割成位数相同的几部分(最后一部分位数可以不同),然后取这几部分的叠加和(去除进位)作为散列地址。数位叠加可以有移位叠加和间界叠加两种方法。移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。
特点是:事先不知道关键字的分布,适合关键字位数较多的情况;
除留取余法:
5、除留取余法:
f(key)=key mod p (p<=m);
取关键字被某个不大于Hash表 长m 的数p 除后所得的余数为Hash地址 。
特点:这是最简单也是最常用的Hash函数构造方法。可以直接取模,也可以在平法法、折叠法之后再取模。
值得注意的是,在使用除留取余法 时,对p 的选择很重要,如果p 选的不好会容易产生同义词 。由经验得知:p 最好选择不大于表长m的一个质数(素数 ) 、或者不包含小于20的质因数的合数。
6、随机数法:
f(key)=Random(key)
选择一个随机函数,取关键字的随机函数值作为Hash地址 ,通常用于关键字长度不同的场合。即
特点:通常,关键字长度不相等时,采用此法构建Hash函数 较为合适。
2.5、处理冲突
如何处理冲突是哈希造表不可缺少的一个方面。现在完整的描述一下处理冲突:
假设哈希表的地址集为,冲突是指由关键字得到的哈希地址为的位置上已存有记录,则“处理冲突”就是为该关键字的记录找到另一个“空”的哈希地址。 在处理冲突的过程中可能得到一个地址序列。即在处理哈希地址的冲突时,若得到的另一个哈希地址仍然发生冲突,则再求下一个地址,若仍然冲突,再求,依次类推,直至不发生冲突为止,则为记录在表中的地址。 (需要注意此定义不太适合链地址法)
1、开放定址法:
所谓的开放地址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大 ,空的散列位置总能找到的,并将记录存入:
f(key)=(f(key)+di) MOD m(i=1,2,3,4,5,6…….m-1);为哈希函数; 为哈希表表长;
为增量序列,有3种取法:
称为线性探测再散列;
fi(key)=(f(key)+di) MOD m(i=1,2,3,4,5,6…….m-1);
称为二次探测再散列;
fi(key)=(f(key)+di) MOD m (di=12,−12,22,−22⋯⋯,q2,−q2,q≤m−1)
称为伪随机探测再散列;
fi(key)=(f(key)+di) MOD m(di是随机数列)
2、再散列函数法:
用多个不同的散列函数
3、链地址法:
将所有关键字为同义词的记录存储在同一线性表中。即在Hash 出来的哈希地址中不直接存Key ,而是存储一个Key 的链表 ,当发生冲突 时,将同义的Key 加入链表 ;
4、公共溢出区:
可以建立一个公共溢出区,用来存放有冲突的Key 。比如设立另一个哈希表,专门用来存放出现冲突的同义词。
2.6、查找及分析
在哈希表上进行查找的过程和哈希造表的过程基本是一致的,过程就不累述了。我们需要看一看其查找的长度。
平均查找长度
虽然哈希表在关键字与记录的存储位置之间建立了直接映像,但由于“冲突”的存在,使得哈希表的查找过程仍然是一个“给定值和关键字进行比较”的过程。因此,仍需以平均查找长度作为衡量哈希表的查找效率的量度;
(还记得上面我们说的“理想情况下”吗?~~ 现实告诉我们,一般情况下,还是不得不需要“比较”!)
查找过程中需要和给定值进行比较的关键字的个数取决于下列三个因素:
- 哈希函数;
- 处理冲突的方法;
- 哈希表的装填因子;
装填因子
在一般情况下,我们设计的哈希函数肯定是尽量均匀的,所以可以不考虑它对平均查找长度的影响。那么,处理冲突方法相同的哈希表,其平均查找长度就依赖于哈希表的装填因子了。其定义如下:装填因子标志哈希表的装满程度
装填因子=表中的记录数/哈希表的长度
直观的看:
- 越小,表明表中还有很多的空单元 ,发生冲突的可能性就越小;
- 越大,代表着表中已填入的元素越多,再填入元素时发生冲突的可能性就越大。那么在查找时,给定值需要比较的关键字的个数就越多;
因此,Hash表的平均查找长度和装填因子有关。有相关文献证明当装填因子在0.5左右的时候,Hash的性能能够达到最优。因此,一般情况下,装填因子取经验值0.5。
Hash表的平均查找时间包括查找成功时的平均查找长度和查找失败时的平均查找长度
查找成功时的ASL=表中每个元素查找成功时的比较次数之和/表中元素个数;
查找不成功时的ASL=在表中查找元素不成功时的平均比较次数,可以理解为向表中插入某个元素,该元素在每个位置都有可能,然后计算出在每个位置能够插入时需要比较的次数,再除以表长即为查找不成功时的平均查找长度。
ASL指的是 平均查找时间
下面举个例子:
关键字序列:(7、8、30、11、18、9、14)
散列函数: H(Key) = (key x 3) MOD 7
装载因子: 0.7
处理冲突:线性探测再散列法
查找成功的ASL计算方法:
因为现在的数据是7个,填充因子是0.7。所以数组大小=7/0.7=10,即写出来的散列表大小为10,下标从0~9。
第一个元素7,带入散列函数,计算得0。
第二个元素8,带入散列函数,计算得3。
第三个元素30,带入散列函数,计算得6。
第四个元素11,带入散列函数,计算得5。
第五个元素18,带入散列函数,计算得5;此时和11冲突,使用线性探测法,得7。
第六个元素9,带入散列函数,计算得6;此时和30冲突,使用线性探测法,得8。
第七个元素14,带入散列函数,计算得0;此时和7冲突,使用线性探测法,得1。
所以散列表:
地址 0 1 2 3 4 5 6 7 8 9 key 7 14 8 11 30 18 9 所以查找成功的计算:
如果查找7,则需要查找1次。
如果查找8,则需要查找1次。
如果查找30,则需要查找1次。
如果查找11,则需要查找1次。
如果查找18,则需要查找3次:第一次查找地址5,第二次查找地址6,第三次查找地址7,查找成功。
如果查找9,则需要查找3次:第一次查找地址6,第二次查找地址7,第三次查找地址8,查找成功。
如果查找地址14,则需要查找2次:第一次查找地址0,第二次查找地址1,查找成功。
所以,ASL=(1+2+1+1+1+3+3)/ 7=12/ 7查找不成功的ASL计算方法:
1. 定义什么叫查找不成功 举个例子来说吧。在已知上面散列表的基础上,如果要查找key为4的关键字。根据散列函数可以计算Hash(key)=Hash(4)=5。此时在地址为5的地方取出那个数字,发现key=11,不等于4。这就说明在装填的时候会发生冲突。根据冲突处理方法,会继续检测地址为6的值,发现key=30,依然不等。这个时候到了地址为6,但是依然没有找到。那么就说明根本就没有key=4这个关键字,说明本次查找不成功。注意:为什么到地址6?因为散列函数中有 mod7 ,对应的地址为0-6,即0~6查找失败的查找次数。 再举一个例子。查找key为0的关键字,根据散列函数可以计算Hash(key)=Hash(0)=0。此时在地址为0的地方取出那个数字,发现key=7,不等于0。这就说明在装填的时候会发生冲突。根据冲突处理方法,会继续检测地址为1的值,发现key=14,依然不等。这个时候到了地址为3,发现为空,依然没有找到。所以停止查找,本次查找不成功。因为如果key=0这个关键字存在的话,依照冲突处理函数,就一定能找到它。总不能丢了吧。
查找地址为0的值所需要的次数为3,
查找地址为1的值所需要的次数为2,
查找地址为2的值所需要的次数为1,
查找地址为3的值所需要的次数为2,
查找地址为4的值所需要的次数为1,
查找地址为5的值所需要的次数为5,
查找地址为6的值所需要的次数为4。
3.计算 查找不成功ASL=(3+2+1+2+1+5+4)/ 7=18/ 7