散列表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 跳房子散列
  • 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】第五章 散列相关推荐

  1. 数据结构和算法分析: 第五章 散列

    散列表的实现常常叫做散列.散列是一种用于以常数平均时间执行插入.删除和查找的技术. 5.1 一般想法 散列表的数据结构是一个包括一些项(item)的具有固定大小的数组.通常查找是对于项的某个部分(即数 ...

  2. 《算法图解》——第五章 散列表(服务器大姨妈来了?第四第五内容传不上去= =!)

        第五章    散列表 1 散列函数(散列映射.映射.字典.关联数组) 散列函数是这样的函数,即无论你给它什么数据,它都还你一个数字.即散列函数"将输入映射到数字" 散列函数 ...

  3. 数据结构与算法笔记(十五)—— 散列(哈希表)

    一.前沿 1.1.直接寻址表 当关键字的全域U比较小时,直接寻址是一种简单而有效的技术.假设某应用要用到一个动态集合,其中每个元素都有一个取自全域U={0,1,-,m-1)的关键字,此处m是一个不很大 ...

  4. 散列、散列函数、散列算法分析

    目录 背景 散列:Hashing 散列表 Hash table 完美散列函数 应用 例子 散列函数的最酷应用--区块链 含义 本质特征 散列函数的设计 折叠法 平方取中法 非数项处理 散列冲突解决方案 ...

  5. 第10章 序列的修改、散列和切片

    # <流畅的Python>读书笔记 # 第10章 序列的修改.散列和切片 转载于:https://www.cnblogs.com/larken/p/10576719.html

  6. 数据库系统概念总结:第十一章 索引与散列

    周末无事水文章,期末备考的总结资料 第十一章 索引与散列 11.1 基本概念 基本的索引类型 –顺序索引:基于值的顺序排序 –散列索引:基于将值平均分布到若干散列同中.一个值所属的散列桶是由一个函数决 ...

  7. c语言每个整数占9列,c语言 第五章 数据类型和表达式.ppt

    c语言 第五章 数据类型和表达式 第五章数据类型和表达式 C语言的基本数据类型 各种基本数据类型的常量和变量的定义 C语言的表达式和各种表达式的求解规则 5.1 数据的存储和基本数据类型 5.1.1数 ...

  8. CoreJava 笔记总结-第五章 继承

    文章目录 第五章 继承 类.超类和子类 定义子类 覆盖方法 子类构造器 多态 阻止继承: `final`类和方法 强制类型转换 抽象类 受保护访问 访问控制修饰符小结 `Object`: 所有类的超类 ...

  9. 第五章.系统安全分析与设计

    目录 第五章.系统安全分析与设计 第一节.信息系统安全属性 第二节.对称加密技术与非对称加密技术 对称加密技术 非对称加密技术 第三节.信息摘要与数字签名 信息摘要 数字签名 第四节.数字信封与PGP ...

最新文章

  1. c语言课程设计链表 文件,C语言课程设计第三节课:指针和链表使用     150809205...
  2. [FlareOn5]Ultimate Minesweeper(dnSpy新玩法)
  3. 如何攻破容器持久化存储挑战?
  4. 硬件编程:77条STM32知识汇总
  5. PHP在接下来的几年内将是主主流的
  6. 【BZOJ4149】[AMPPZ2014]Global Warming 单调栈+RMQ+二分
  7. 苹果Mac专业的字体管理应用:​​​​RightFont
  8. Java/Hbase + C云平台架构设计_十项法则
  9. 关于 Eureka 2.x,别再人云亦云了!
  10. 黑马程序员java学生管理系统
  11. python聚类分析实例_Biopython - 聚类分析
  12. 自学Java day12 使用jvav实现链表 从jvav到架构师
  13. 怎样在线快速缩小动图大小?怎样在线压缩gif图片?
  14. 私人浏览黄网算不算违法?
  15. HTML5开发系列(4) 之 样式表的三种类型
  16. 腾讯TEG--C++面试知识点总结
  17. 计算机电路中bga是什么,bga是什么
  18. 【9】核心易中期刊推荐——图像视觉与图形可视化
  19. pcb图3d模型丢失的情况解决
  20. 阿里云ACA大数据助理工程师认证学习笔记和题

热门文章

  1. 《密码与安全新技术专题》第三周作业
  2. 数据分析-数据分析方法
  3. mysql中flush用法_mysql flush用法
  4. 计算机保研面试自我介绍,计算机保研面试英文自我介绍.doc
  5. 毕业生就业管理系统的设计与实现
  6. 【小程序学习】uni-app无法启动微信开发者工具
  7. 今天,小灰37岁了!
  8. 红米k30s至尊纪念版和红米k30pro变焦版的区别 哪个好
  9. JCenter已经提桶跑路,是时候学会上传到Maven Central了
  10. PhotoshopCC2018教程73课时-1.57G