【ADT】第五章 散列
散列表ADT是一个包含一些项的具有固定大小的数组
散列是一种以常数平均时间O(1)执行插入、删除、查找的技术
JavaCollection中基于散列技术实现了HashSet、HashMap
不支持排序、findMin、findMax等操作
散列表一般是基于HashCode实现散列函数的,故散列表中存储的对象需要hashcode()和equals()方法
- 1 散列ADT
- 1.1 基本了解
- 1.2 散列的基本想法
- 1.3 散列的问题
- 散列表大小的选择:
- 散列函数如何计算
- 散列函数和HashCode
- 通用散列法(通用散列函数)
- 2 如何存储冲突值
- 2.1 分离链接法
- 2.2 不用链表的散列表(探测散列表)
- 2.2.1 线性散列表
- 2.2.2 平方散列表
- 实现
- 2.2.3 双散列
- 3 散列表的扩充
- 4 JavaCollection中的实现
- 4.1 HashSet
- 4.2 HashMap
- 5 完美散列
- 5.1 布谷鸟散列
- 查找
- 删除
- 插入
- 插入过程的流程
- 实现
- 5.2 跳房子散列
- 5.1 布谷鸟散列
- 6 可扩散列
1 散列ADT
1.1 基本了解
散列表ADT是一个包含一些项的具有固定大小的数组
查找操作是通过项的某个部分进行的,这个部分叫做关键字
1.2 散列的基本想法
把散列表的大小记作TableSize
把每个关键字映射从0到TableSize-1这个范围的某个数,并放入数组中对应的单元中的技术叫做散列
其中映射到(0 ~ TableSize-1)的函数叫做散列函数(Hash Function)
这个散列函数在一般情况下就是Key mod TableSize,Key一般下就是对象的HashCode()了
1.3 散列的问题
但是散列ADT中存在一个很大的问题就是散列冲突:TableSize必然不能无限大,甚至不能过大,这样必然会有多个关键字映射到同一个值,这样如何存储冲突值就成了一个需要探讨的问题;另外如何能尽可能利用TableSize中的每一个单元,而不是将冲突集中到其中几个单元,剩下的单元利用不充分造成资源浪费
利用散列表需要考虑的问题:
1 如何存储冲突值
2 散列表的大小如何选择
3 散列函数如何计算才能尽可能减小冲突
我们先说明后两个问题:
散列表大小的选择:
散列表的大小选择要求尽可能平均分布,举例若散列表大小为10而关键字都以0为个位,那么所有的关键字都会在0位冲突,其他单元又严重浪费,比较好的办法就是:
保证表的大小是素数(素数:除了1和自己不能被整除)(一般取17以上的素数)
原因:
1 如果关键字是随机的,TableSize是多少其实并无所谓,然而在现实中关键字往往有某种规律,例如大量的等差数列,那么公差和模数不互质的时候发生碰撞的概率会变大,而用质数就可以很大程度上回避这个问题。
2 一般地说, 当模数非常大的时候, 取什么数关系不太大, 质数合数都有不错的结果,但是一般情况下模数不会 “足够大” , 这个时候, “所有” 17以上的素数都有不错的结果, 而很多合数也有不错的结果, 但是个别一些合数结果会非常非常差,冲突非常多.。因此为了稳妥起见, 取17以上的素数.
//是否为素数private boolean isPrime(int num) {if (num == 2 || num == 3) {return true;}if (num % 6 != 1 && num % 6 != 5) {return false;}for (int i = 5; i * i <= num; i += 6) {if (num % i == 0 || num % (i + 2) == 0) {return false;}}return true;}//n后面的下一个素数private int nextPrime(int n) {boolean state = isPrime(n);while (!state) {state = isPrime(++n);}return n;}
散列函数如何计算
一般情况下的散列函数=对象的Key mod TableSize(对象的Key = 对象的HashCode)
public int myHash(T t) {int hashValue = t.hashCode() % list.length;if(hashValue < 0) { //考虑溢出为负数hashValue += list.length;}return hashValue;}
当然对象的HashCode可以依据自身重写
但要记住的是再怎么重写他也是返回的Key,是在对象类中定义的
散列函数却是在实现的散列表中定义的myHash(),哪怕令 myHash = HashCode ,也要在思想上有所区分
散列函数和HashCode
HashTable中的容量并不等同于存储元素的个数,散列的作用在于HashTable中只有11个位置,数组中下标0-10,通过散列函数决定这个元素应该放置在数组中哪个下标的单元中,获取的是数组的下标
一个对象的HashCode是表示这个元素是否为新元素的标记,他是对象的Key
一般情况下Set、Map中不存放重复元素(除非有特殊要求),如果HashCode相等表示是一个对象,则不做插入操作
通过散列函数获取存储位置下标,再与该位置中存储元素的hashcode一一对比,若hashcode不相等表示是新元素插入,若hashcode相等表示是重复元素
通用散列法(通用散列函数)
为了尽可能减小冲突引入通用散列法
H = [(a * key + b) mod p ] mod M
其中1 key为对象HashCode
2 M为散列表TableSize
3 p为大于TableSize的任何素数,一般情况下我们使用梅森数(梅森数:n是一个素数 [2n - 1]也是一个素数)例如:25 - 1 ;231 - 1 ;261 - 1;289 - 1…..,利用梅森数求解mod会十分方便
以梅森数2n - 1 为例,梅森数在乘法和mod上可以利用位移运算简化运算时间
1 乘法 : K * 2n - 1 = K * 2n - K;其中 2n=2 << n(左移n位)
2 mod: K mod 2n - 1 = K/2n + K mod 2n;
其中: K/2n = 2 >> n (右移n位) ,K mod 2n = K & 2n-1 (K 和 2n-1按位与运算 )
4 (a,b)是一对系数,a、b为随机选取的,其中 1<= a <= p-1、 0<= b <= p-1
最后集中讨论第一个问题,如何存储冲突的值
2 如何存储冲突值
2.1 分离链接法
将散列到同一个值得所有元素保留在一个表中(比如存在一个List上),那么用来存储的数组就是一个List数组(List[])
具体实现看GitHub
2.2 不用链表的散列表(探测散列表)
探测散列表的基本思想:不使用链表而是尝试另外的一些单元,关键字冲突之后将冲突的元素插入到距离冲突位置d的数组位置上
d = f(i) mod TableSize 【 i 是冲突的次数0 1 2 3 ……】
或者说探测散列表散射函数 H = (HashCode + F(i))mod TableSize
依据F(i)的不同分为以下三种:
2.2.1 线性散列表
F(i) = i
如果发生冲突,将放入下一个地址,如果下一个地址依然冲突,则再次下移一个地址,直到存放到空闲地址为止
只要表足够大就一定能找到一个空闲的单元,但是这样产生的问题在于:每次查找空闲单元时都会花费相当多的时间,即使表相对较空,占据的单元由于冲突时下移一位会连在一起构成一次聚集区,这样散列到这个区块的元素要经过多次查找选择才能解决冲突
2.2.2 平方散列表
F(i) = i2
平方探测法可以解决线性探测中的一次聚集问题
当元素在位置 j 发生冲突时,首先在 j+1的位置探测,如果依然冲突则令i=2,在 j+4的位置探测,如果再次冲突 i=3 ,在 j+9的位置探测。。。。直至空闲元素
平方散列表将每次探测间距分隔开,由此解决一次聚集问题
实现
平方散列表实现查找、删除、插入操作,其中注意一点对平方散列表进行删除时,由于冲突元素有可能经过这个被删除元素存放到较下面的位置,如果真的删除这个元素,会影响散列表的结构对后面的探测造成影响,因此在平方散列表中实施的是懒惰删除:不实际删除这个元素,仅仅给一个标记证明是被删除的元素
private class HashEntrySet<T> {private T data;private boolean activeIdex;//删除 不存活 false//我们需要对activeIndex设置set方法public void setActiveIdex(Boolean activeIdex){this.activeIdex = activeIdex;}public HashEntrySet(T t) {this.data = t;this.activeIdex = true;}}
遍历时,在散列函数映射的位置开始以i2为步长遍历查找
public int contains(T t) { //获取t的位置int index = myHash(t);//获取这个数,本该存放的位置,然后按照冲突i=1 i=2 i=3的向后遍历int i = 1;while( list[index] != null &&!list[index].data.equals(t)){//如果查到的list[index]==null则表示不存在这个数,因为如果有这个数这个位置不会为空//同样如果插入,插入的位置也应该是这个地方index = index + (2*i-1); // i^2 - (i-1)^2 --> 2i-1i++;if(index > list.length) {index -= list.length;//例如9再向后遍历应该是0,循环遍历}}return index;}
插入操作就是先查找如果查找到元素且是被删除的,将删除位set false,如果查找不到就新加一个EntrySet
删除操作是查找到元素并把删除位set true
2.2.3 双散列
双散列就是步长F(i)不再是基于i的一个固定简单函数,而是一个复杂的散射函数
F(i)=i * hash2(x)
3 散列表的扩充
散列表的大小并不是一成不变的,当存储元素达到一定程度(或者叫做填充因子,以及占据的单元/散列表的单元总量),一般情况下填充因子达到0.5时需要扩充散列表数组容量,我们这个过程叫做再散列
在这个过程中要注意:
1 新创建的散列表大小同样也需要是质数,是原表大小两倍之后的第一个质数
2 散列函数=xx mod TableSize,故散列表大小改变之后,散列函数依然会变化,原散列表数组中的元素要经过新的散列函数映射到新的位置
private void expand() {T[] oldList = (T[])this.list;this.list = new Object[nextPrime(this.list.length)];this.size = 0;for(T t : oldList) {if(t != null) {insert(t);}}}
4 JavaCollection中的实现
JavaCollection中HashSet和HashMap通常是分离链接散列实现的
就像在最开头说明的,集合中存放的对象必须实现hashcode和equals方法
HashSet、HashMap不支持顺序排序,所以要在依照有序查看这一操作不重要的时候选择这两个类
散列表中费时最多的计算过程就是HashCode方法,然而在String类有一个重要的优化(闪存散列代码):在String内部存储它的HashCode值,该值初始为0,若HashCode函数被调用那么这个值会一直被记住,当调用HashCode第二次计算时直接调用,避免重新计算,String类具有这种机制的原因在于String类是不可改变的,若String发生改变会新生成一个String对象存储新的HashCode值。
4.1 HashSet
4.2 HashMap
一般情况下,HashMap的性能优于TreeMap的性能,但是不一定肯定,在TreeMap和HashMap都可以接受的情况下,使用接口类型Map进行变量的声明,然后将TreeMap的实例变成HashMap的实例并进行计时测试,选择优者。
PPS:TreeMap的实例变成HashMap的实例。。。把元素一个个添加。。直接强制转换是不可能的
5 完美散列
以上的散列实现都是在合理的散列函数下,期望插入删除操作的平均花销是O(1),但是仍不排除部分极坏的情况时间复杂度会超出O(1)
但是对于某些应用下,要求即便是最坏情况下也要O(1),这就要求我们进一步缩短花销时间,这就引出了完美散列
完美散列的基本思想:
1 在主散列数组的每个单元中再存放一个散列列表数组(二级散列表),用来存放冲突的值
2 二级散列表大小是对应的主散列表单元中元素的平方(也就是冲突元素数量的平方)
3 每个二级散列表用一个不同的散列函数映射直到没有冲突,可以存放在主散列表单元的元素
4 如果产生冲突的次数过高(多于设定值),主散列表可以重新用一个不同的散列函数重新构建
完美散列有一个前提是假定要存储的N项是已知的,这样我们才方便选择映射函数、构建二级散列表直至没有冲突(静态完美散列:所有Key已知且固定)
涉及到动态插入删除等操作时,我们使用下面两种更新方法:布谷鸟散列、跳房子散列
5.1 布谷鸟散列
布谷鸟散列是基于一个定理:将N项放入N个盒子,盒子容量最大期望值为O(logN/log logN),但是如果每次投放时随机选择两个盒子,将该项放入比较空的那一个盒子,则盒子容量最大期望值为O(log logN)
于是产生了布谷鸟散列:维护两个散列表,并且每张表有自己独立的散列函数(两张表的散列函数要明显不同否则在表A的i位置冲突,到表B还是i位置冲突,这样就不行了),可以把每个项分配到每个表的一个位置(存储在两张表中映射的两个位置之一)
布谷鸟散列的特性:一项总会被存储到两张表映射的两个位置之一(好好理解)
查找
布谷鸟散列中一次查找最多需要访问两次表,一次表A,一次表B,而且访问的位置一定是表A通过映射函数A得到的位置i 和 表B通过映射函数B得到的位置 j ,因为上面的特性这个元素一点存储在这两个位置之一
删除
删除的实现也十分简单,找到这个元素直接删除置空,不需要我们在平方探测中使用的懒惰删除,原因还是特性,这个元素在表中本来就是应该存在在这个位置的,没有冲突之说也不会破坏结构,故直接删除
插入
插入操作相对于查找、删除稍微复杂一些
插入过程的流程
将元素T插入表A的 i 位置
如果无元素直接插入
如果i位置上有元素O,直接替换掉这个元素令A[i]=T
,然后将元素O插入到表B中
元素O通过表B的映射函数B映射到位置j
如果j位置没有元素直接插入
如果j位置上有元素K,直接替换这个元素,令B[j]=O
,然后将元素K插入表A中
。。。。。
直到完成
在这个插入流程中有几个问题:
1 循环替换插入时存在几率:A换掉B,B换掉C,C换掉A这样的死循环,这时说明表A 表B的映射函数差异值还是不够,需要新的散列函数重新散列
2 经实验分析,表的装填因子小于0.5时,发生上述死循环的概率很低
实现
就像我们上面说到的那两个问题,在实现插入操作时除了替换插入,我们还需要注意:
1 当表内的填充因子超过设定值(MAX_LOAD)时,需要扩充表(expand)
2 当替换操作的次数超过设定值(MAX_CHANGE)时,我们认为需要更换散列函数,需要重新生成表A 表B的散列函数并重新散列(rebuild这个过程不改变表的大小,只是用新的散列函数去填充)
3 如果再散列的次数超过限定值(MAX_NEW_COUNT)时,说明按照此刻表的大小很难找到不冲突的散列函数,这时选择将表扩展(expand)
public void insert(T t) {if(contains(t) != -1){return;}if(this.size >= this.list.length * Max_LOAD) {expand();}//具体的插入int newCount = 0;//生成新散列的次数while(true) {for(int change = 0; change < MAX_CHANGE; change++) { //可以交换MAX_ChanGE次//查找每一个散列函数 看有没有空位置for(int i= 0; i < this.functionNumber; i++) {int index = myHash(t,i);if(list[index] == null) {list[index] = t;this.size++;return;}}//找不到可用位置,就要替换,这里采用随机替换int oldIndex = -1;int newIndex = -1;int count = 0;//随机生成多少次while(count ++ < 5 && newIndex == oldIndex){ //随机newIndex = myHash(t,new Random().nextInt(this.functionNumber));//随机找一个散列}//找到交换的newIndexoldIndex = newIndex;T tToChange = (T)list[oldIndex];list[oldIndex] = t;t = tToChange;}//交换MAXCHANGE次依然冲突,根据条件选择是生成新的散列表还是扩充if(newCount < MAX_NEW_COUNT) { //如果没到5 生成新散列reBuild();}else{//否则扩充newCount = 0;expand();}}}
5.2 跳房子散列
跳房子散列是对线性探测算法的改进,线性探测中因为冲突本该插入的位置i,插入 i + d的位置,而跳房子散列的基本思想就是为这个距离d增加一个上界
跳房子散列设置一个数组Hop[n],这个n就是为距离d增加的上界,若插入的位置距离散列位置i上界为4(即可以 i 、 i+1、i+2、i+3)
Hop[ i ]数组即为4位,从左到右第 j 位表示散列为i的元素实际存放在 i + (j - 1)的位置上
Hop[i] | 散列为i的实际存放位置 |
---|---|
0000 | 无散列为i的元素 |
1000 | 存放在位置i |
0100 | 存放在位置i+1 |
0010 | 存放在位置i+2 |
0001 | 存放在位置i+3 |
1100 | 有两个元素散列为i,存放在位置i和位置i+1 |
1110 | 有三个元素散列为i,存放在位置i,位置i+1,位置i+2 |
….. | 等等。。不再列述 |
插入流程:
元素T通过散列列表获取位置 i
经过线性探测获得可以插入的位置 i + d
如果d小于上界,则可以直接插入
如果d不小于上界,则需要对散列表结构调整:
1 从位置 i + d 开始自顶向上查找,遍历Hop[i]最后一位不是1的元(除去0000,因为无元素散列不能带给我们任何咨询),找到可以下移到下面的空单元的元素O(令最后一位尽可能为1,但是不要超过i+d),更新Hop[],如此便可以释放当前的单元空间 j
2 在j的基础上继续向上遍历最后一位不是1的元素,找到可以下移到刚刚释放的单元j上的元素K,更新Hop[],释放元素K的空间
3 如此循环直到释放出一个空间可以存放元素T
6 可扩散列
可扩散列是用于数据量太大以至于装不进主存的情况。。
[感觉先用不到,占坑吧。。。。]
【ADT】第五章 散列相关推荐
- 数据结构和算法分析: 第五章 散列
散列表的实现常常叫做散列.散列是一种用于以常数平均时间执行插入.删除和查找的技术. 5.1 一般想法 散列表的数据结构是一个包括一些项(item)的具有固定大小的数组.通常查找是对于项的某个部分(即数 ...
- 《算法图解》——第五章 散列表(服务器大姨妈来了?第四第五内容传不上去= =!)
第五章 散列表 1 散列函数(散列映射.映射.字典.关联数组) 散列函数是这样的函数,即无论你给它什么数据,它都还你一个数字.即散列函数"将输入映射到数字" 散列函数 ...
- 数据结构与算法笔记(十五)—— 散列(哈希表)
一.前沿 1.1.直接寻址表 当关键字的全域U比较小时,直接寻址是一种简单而有效的技术.假设某应用要用到一个动态集合,其中每个元素都有一个取自全域U={0,1,-,m-1)的关键字,此处m是一个不很大 ...
- 散列、散列函数、散列算法分析
目录 背景 散列:Hashing 散列表 Hash table 完美散列函数 应用 例子 散列函数的最酷应用--区块链 含义 本质特征 散列函数的设计 折叠法 平方取中法 非数项处理 散列冲突解决方案 ...
- 第10章 序列的修改、散列和切片
# <流畅的Python>读书笔记 # 第10章 序列的修改.散列和切片 转载于:https://www.cnblogs.com/larken/p/10576719.html
- 数据库系统概念总结:第十一章 索引与散列
周末无事水文章,期末备考的总结资料 第十一章 索引与散列 11.1 基本概念 基本的索引类型 –顺序索引:基于值的顺序排序 –散列索引:基于将值平均分布到若干散列同中.一个值所属的散列桶是由一个函数决 ...
- c语言每个整数占9列,c语言 第五章 数据类型和表达式.ppt
c语言 第五章 数据类型和表达式 第五章数据类型和表达式 C语言的基本数据类型 各种基本数据类型的常量和变量的定义 C语言的表达式和各种表达式的求解规则 5.1 数据的存储和基本数据类型 5.1.1数 ...
- CoreJava 笔记总结-第五章 继承
文章目录 第五章 继承 类.超类和子类 定义子类 覆盖方法 子类构造器 多态 阻止继承: `final`类和方法 强制类型转换 抽象类 受保护访问 访问控制修饰符小结 `Object`: 所有类的超类 ...
- 第五章.系统安全分析与设计
目录 第五章.系统安全分析与设计 第一节.信息系统安全属性 第二节.对称加密技术与非对称加密技术 对称加密技术 非对称加密技术 第三节.信息摘要与数字签名 信息摘要 数字签名 第四节.数字信封与PGP ...
最新文章
- c语言课程设计链表 文件,C语言课程设计第三节课:指针和链表使用 150809205...
- [FlareOn5]Ultimate Minesweeper(dnSpy新玩法)
- 如何攻破容器持久化存储挑战?
- 硬件编程:77条STM32知识汇总
- PHP在接下来的几年内将是主主流的
- 【BZOJ4149】[AMPPZ2014]Global Warming 单调栈+RMQ+二分
- 苹果Mac专业的字体管理应用:​​​​RightFont
- Java/Hbase + C云平台架构设计_十项法则
- 关于 Eureka 2.x,别再人云亦云了!
- 黑马程序员java学生管理系统
- python聚类分析实例_Biopython - 聚类分析
- 自学Java day12 使用jvav实现链表 从jvav到架构师
- 怎样在线快速缩小动图大小?怎样在线压缩gif图片?
- 私人浏览黄网算不算违法?
- HTML5开发系列(4) 之 样式表的三种类型
- 腾讯TEG--C++面试知识点总结
- 计算机电路中bga是什么,bga是什么
- 【9】核心易中期刊推荐——图像视觉与图形可视化
- pcb图3d模型丢失的情况解决
- 阿里云ACA大数据助理工程师认证学习笔记和题