1.基本概念
2. 哈希函数
2.1 直接寻址法
2.2 数字分析法
2.3 平方取中法
2.4 折叠法
2.5 随机数法
2.6 除留余数法
3. 哈希查找
3.1 哈希查找的操作步骤
3.2 哈希表查找效率
3.3 装填因子
3.4 平均查找长度(ASL)
4. 解决哈希冲突的方法
4.1 开放地址法
4.2 再哈希法
4.3 拉链法
4.4 建立公共溢出区
4.5 线性探测法
5. 哈希算法
5.1 原理与应用
5.2 哈希算法特点
6. HashMap
6.1 工作原理
6.2 使用HashMap的原因
6.3 B+Tree/Hash_Map/STL_Map区别
6.4 相关例题
7. 红黑树
8. 三元组稀疏矩阵存储
Hash,也叫哈希或散列,就是把任意长度的输入(也叫预映射),通过散列算法,变换成固定长度的输出,该输出就是散列值。
若结构中存在和关键字key相等的记录,则必定在H(key)的存储位置上。称该H(key)为哈希函数。
根据设定的哈希函数H(key)和处理冲突方法将一组关键字映射到一个有限的地址区间上,并以关键字在地址区间中的象作为记录在表中的存储位置,这种表称为哈希表或散列表。这一映像称为散列,所得存储位置称为哈希地址或散列地址。
对不同的关键字可能得到同一散列地址,即key1≠key2,而f(key1)=f(key2),这种现象称碰撞。
哈希函数能使数据序列的访问过程更加迅速有效,通过哈希函数,数据元素将被更快地定位。
散列法存储的思想是由关键字和散列函数(即哈希函数)共同决定数据的存储地址的。
常见的哈希函数有:
取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a*key + b,其中a,b为常数,这种哈希函数为自身函数。
例:有一个从1到100岁的人口数字统计表中,其中,年龄作为关键字,哈希函数取关键字自身。
有学生的生日数据如下:
年 月 日
75 10 03
75 11 23
76 03 02
76 07 12
75 04 21
76 02 15
经分析,第1,2,3位数重复的可能性大,取这三位造成的冲突的机会增加,所以尽量不取前3位较好。
取关键字平方后的中间几位为哈希地址。
将关键字分隔成位数相同的几部分(最后一部分的位数可以不同),然后把这几部分的叠加和(舍去进位)作为哈希地址。
例:每一种图文图书都有一个国际标准图书编号,它是一个10位的十进制数字,若要以它作关键字建立一个哈希表,当馆藏种类不到10000时,可采用折叠法构造一个四位数的哈希函数。
选择一个随机函数,取关键字的随机函数值作为它的哈希地址,即H(key)=random(key),其中random为随机函数。通常用于关键字长度不等时的场合。
取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址。即H(key)=key MOD p(p<=m)。
在哈希函数进行模除取余时,最好取素数进行模除。其原因和计算机的输入有关,因为计算机输入不是所有数都是随机的,而是有某种规律。因为存在大量的while和for循环,而素数对于这样的数处理起立比合数要好,而且素数一般为3。
哈希函数的构造不是越复杂越好,因为哈希函数越复杂,取得关键字地址所消耗的时间越长,可能对哈希算法性能造成一定的影响。因此选择哈希函数的时候,应该多方面权衡,选择合适的哈希函数。
哈希查找是通过计算数据元素的存储地址进行查找的一种方法。
① 给定的哈希函数构造哈希表。
② 根据选择的冲突处理方法解决地址冲突。
③ 在哈希表的基础上执行哈希查找。
step1先取数据元素的关键字key,计算哈希函数值。若该地址对应的存储空间没有被占用,则将元素存入;否则执行step2解决冲突。step2根据选择的冲突处理方法,计算关键字key的下一个存储地址。若下一个存储地址仍被占用,则继续执行step2,直到找到能用的存储地址为止。
因为有些数据本身是无法排序的(如图像),有些数据是很难比较的(如图像),如果数据本身是无法排序的,就不能对它们进行比较查询。如果数据时很难比较的,即使采用折半查找,要比较的次数也是非常多的。因此哈希查找并不查找数据本身,而是先将数据映射为一个整数(它的哈希值),并将哈希值相同的数据存放在同一个位置,即以哈希值为索引构造的一个数组。
在哈希查找的过程中,只需先将要查找的数据映射为它的哈希值,然后查找具有这个哈希值,这就大大减少了查找次数。
哈希查找可以在外存中查找,可以用哈希表映射到文件,分级查找。
虽然哈希查找就是尽量避免比较直接找到位置,但因为一旦有hash冲突的存在,就会有比较操作,所以在哈希查找中,“比较”操作一般是不可避免的。
哈希表的查找效率主要取决于哈希函数、处理冲突的方法和装填因子。
为了提高哈希表的查找效率,可以采取的正确措施有:
① 减小装填(载)因子
② 设计冲突(碰撞)少的散列函数
③ 处理冲突(碰撞)时避免产生聚集(堆积)现象
不同结构获得任意指针值:
二叉树排序树中,查找的平均时间复杂度是O(logn);
对于栈和队列来说,查找是把元素挨个出栈或队列,故平均时间复杂度O(n);
哈希表直接通过关键码查找元素,平均为O(1)。
装填因子的计算公式=关键字个数/表长度。即装填因子是衡量哈希的好坏标准之一,还和hash表的平均查找长度有关。如果要提高效率,可以减小装填因子,即可以减少关键字个数或提高表长度。
装填因子反应的是空间利用率。
例:你要对5个对象进行hash,而内存中,准备了20个位置,那么还有15个空位,最后装填因子就是5/20 = 0.25,所以装填因子越小,产生冲突的可能越小。
ASL(Average Search Length),即平均查找长度,在查找运算中,由于所费时间在关键字的比较上,所以把平均需要和待查找值比较的关键字次数称为平均查找长度。
定义为:
其中n为查找表中元素个数,pi为查找第i个元素的概率,通常假设查找每个元素的概率相同,即pi=1/n,ci是找到第i个元素的比较次数。
一个算法的ASL越大,性能越差,ASL越小,性能越好。
例:设哈希表长为8,哈希函数为Hash (key)=key%7。初始记录关键字序列为(32,24,15,27,20,13),用链地址法作为解决冲突方法的平均查找长度是( )。
链地址法作为解决冲突方法:冲突以后变成链表,查询次数增加
32%7=44位置为空,存入(查一次)
24%7=33位置为空,存入(查一次)
15%7=11位置为空,存入(查一次)
27%7=66位置为空,存入(查一次)
20%7=66位置不为空,向下查找,7位置为空,存入(查两次)
13%7=66位置不为空,向下查找,7位置不为空,继续查找,5位置为空(就近原则),存入(查三次)。
ASL=(14+21+3*1)/6=1.5
当两个不同的数据元素的哈希值相同时,就会发生冲突。为减少发生冲突的可能性,哈希函数应该将数据尽可能地分散地映射到哈希表的每一个选项中。解决冲突的方法有以下4种:
基本原理是:当关键字key的哈希地址出现冲突时,计算key的下一个存储地址w1,假如w1被占用;则计算key的下一个存储地址w2,如果w2仍被占用,继续计算key的存储地址w3,w4……直至找到能用的存储地址,将相应元素放入其中。
在开放地址法构造的散列表,删除结点不能简单地将被删的结点的空间置为空,否则将截断在它之后填入散列表的同义词结点的查找路径。因为开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不是真正的删除结点。
这种方法就是同时构造多个不同的哈希函数,Hi=RH(key) i=1,2,…,k;当哈希地址H1=RH(key)发生冲突时,再计算H2=RH(key),……,直到冲突不再产生。这种方法不容易产生聚集,但增加了计算时间。
也叫链地址法,将哈希值相同的数据元素存放在一个链表中,在查找哈希表的过程中,当查找到这个链表时,必须采用线性查找方法。链地址法适用于经常进行插入和删除的情况。
对于拉链法来说,数组的每一个结点指向一个链表,链表中的每一个结点都存储了散列值为该索引的键值对,而链表的维护需要结点之间的指针来维护。
拉链法优点:
1)、由于链地址法中各链表上的结点空间是动态申请的,故更适合造表前无法确定表长的情况。
2)、链地址法处理冲突简单,且无堆积现象,即非同义词不会发生冲突,因此平均查找长度较短。
3)、开放地址法为减少冲突,要求装填因子较小,故当结点规模大会浪费很多空间。而链地址法可取装填因子大于等于1,因此节省空间。
4)、在用链地址法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上的相应的结点即可。
拉链法缺点:
指针需要额外的空间,故当结点规模较小时,开放地址法较为节省空间。而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放地址法中的冲突,从而提高平均查找速度。
基本原理是将哈希表分为基本表和溢出表,凡是和基本表发生冲突的元素,一律填入溢出表。
基本思想:将散列表T[0…m-1]看成是一个循环向量,若初始探查的地址为d(即h(key)=d),则最长的探查序列为:d,d+l,d+2,…,m-1,0,1,…,d-1。即:探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],…,直到T[m-1],此后又循环到T[0],T[1],…,直到探查到T[d-1]为止。
即当发生冲突时,从给冲突位置逐个向后查找,类似于循环数组,直到找到合适的位置或找遍整个表都为找到合适的位置。
探查过程终止于三种情况:
若当前探查的地址为空,则表示查找失败,但可以将key插入其中;若当前探查的地址中含有key,则查找成功,但意味着插入失败;若探查到T[d-1]时仍为发现空地址也未找到key,意味着查找和插入都失败。
此方法的却易发生堆积现象,堆积就是存入哈希表的数据在表中连成一片。
例1:设哈希表长m=13,哈希函数H(key)=key MOD 11。表中已有4个节点:addr(16)=5,addr(28)=6,addr(84)=7,addr(19)=8其余地址为空,如用线性探测再散列处理冲突,则关键字为38的地址为( 9 )。
线性探测再散列方法的特点是:冲突发生时,顺序查看表中的下一个单元,直到找到一个空单元或查遍全表。38 MOD 11 = 5,5和addr(16)=5冲突,继续查找;addr(28)=6,addr(84)=7,addr(19)=8,即6,7,8不为空,继续查找;9为空,存入。
例2:关键字序列为{12,11,19,23,1,6,10},哈希函数为H(key)=key MOD 11,用链地址法构造哈希表,哈希地址为1的链中有( 3 )个记录。
释:
12%11=1;
11%11=0;
19%11=8;
23%11=1;
1%11=1;
6%11=6;
10%11=10;
哈希地址为1的链中有 12,23,1。
Hash算法也被称为散列算法,Hash算法虽然被称为算法,但实际上它更像是一种思想。Hash算法没有一个固定的公式,只要符合散列思想的算法都可以被称为是Hash算法。
常见的hash算法有:MD4,MD5,SHA-1及其他。
哈希算法可以以比较短的信息来保证文件唯一性的标识,这种标识与文件的每一个字节都相关,难以找到逆向规律。
哈希算法最重要的用途是证书、文档、密码等高安全系数的内容添加加密保护。原因在于哈希算法的不可逆性,即不能根据一段哈希算法得到的数据来获得原有的文件,也不可能再三地创造一个文件使其与一段目标文件相一致。现在大部分网络部署和版本控制工具都在使用散列算法来保证文件可靠性;另一方面,在进行文件系统同步、备份等工具时,使用哈希算法来标志文件唯一性能减少系统开销,这一点在很多云存储服务器中都有应用。
正向快速:给定明文和hash算法,在有限时间和有限资源内能计算出hash值。
释:明文是指没有加密的文字或字符串,也就是未经加密算法的原消息。
逆向困难:给定若干hash值,在有限时间内很难逆推出明文。
输入敏感:原始输入信息修改一点信息,产生的hash值看起来应该都有很大不同。
冲突避免:很难找到两段内容不同的明文,使得它们的hash值一致(发生冲突),即对于任意两个不同的数据块,其hash值相同的可能性极小;而对于一个给定的数据块,找到和它hash值相同的数据块极为困难。
HashMap是使用put(key,value)存储对象到HashMap中,再使用get(key)从HashMap中获取对象。当给put()方法传递键和值时,先对键调用hashCode()方法,计算并返回的hashCode是用于找到Map数组的bucket位置来储存Node对象。
HashMap是在bucket中储存对象和值对象作为Map.Node。
以下是具体的put()方法的过程(JDK1.8版):
① 对key求hash值,然后计算下表;
② 如果没有发生冲突,直接放入哈希桶中;
③ 如果发生冲突,以链表的方式链接到后面;
④ 如果链表长度超过阙值(8),就把链表转为红黑树;链表 长度低于6,就把红黑树转回链表;
⑤ 如果结点已经存在就替换旧值;
⑥ 如果哈希表桶已满,需要resize扩充后重排。
get()方法具体过程如下:
当调用get()方法,HashMaph会使用键对象的hashcode找到bucket位置,找到bucket位置后,会调用keys.equals()方法去找到链表中正确的结点,最终找到要找的值对象。
HashMap是一个散列桶(数组和链表),它存储的内容是键值对(key-value)映射。
HashMap采用了数组和链表的数据结构,能在查询和修改方面继承了数组的线性查找和链表的寻址修改。
HashMap是非synchronized,所以HashMap查询很快。
HashMap可以接受null键和值,而hashtable不能。
1)、Hash操作能根据散列值直接定位数据的存储地址,设计良好的hash表能在常数级时间下找到需要的数据,但是更适合于内存中查找。
2)、B+Tree是一种树状的数据结构,适合做索引,对磁盘数据来说,索引查找是比较高校的。
3)、STL_Map的内部实现是一棵红黑树,但是只是一棵在内存中建立二叉树的树,不能用于磁盘操作,而且内存查找性能比不上Hash查找。
例1:如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
默认的负载因子大小为0.75,也就是说一个map填满了75%的bucket时,和其他集合类(如ArrayList)一样,将会创建原来HashMap大小两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中,这个过程叫作rehashing。因为rehashing调用hash方法找到新的bucket位置,所以其值只可能在原下标的位置或下标为(原下标+原容量)的位置。
例2:重新调整HashMap大小存在什么问题?
当重新调整HashMap大小的时候,会存在条件竞争。因为当如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置时,为了避免尾部遍历(tail traversing),HashMap并不会将元素放在链表的尾部,而是放在头部。如果条件竞争发生,多线程进入死循环,所以在多线程环境下不使用HashMap。
例3:为什么多线程会导致死循环,它是怎么发生的?
HashMap的容量是有限的,当经过多次元素插入,使得HashMap达到一定饱和度时,key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展长度(Resize)。
Resize方法为:1、扩容:创建一个新的Entry的空数组,长度是原数组的两倍;2、Rehash:遍历原Entry数组,把所有的Entry重新Hash到新数组。
拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?
选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构,会造成很深的问题,遍历查找会非常慢。而红黑树在插入新数据后可能需要左旋、右转、变色这些操作来保持平衡,引入红黑树就是为了查找数据块,解决链表查询深度的问题。红黑树属于平衡二叉树,为了保持“平衡”需要损耗资源,但所损耗的资源要比遍历线性链表要少。所以如果长度大于8时,使用红黑树,但如果链表长度很短时,使用红黑树查找速度回变慢。
额外:广度优先搜索借助队列,深度优先搜索借助栈。
设mn矩阵中有t个非零元素且t<
要唯一的表示一个稀疏矩阵,还需要存储三元组表的同时存储该矩阵的行、列,为了运算方便,矩阵的非零元素的个数也同时存储。