0
点赞
收藏
分享

微信扫一扫

数据结构与算法 — 认识哈希表、地址的冲突、链地址法、开放地址法、哈希化的效率

目录

一. 认识哈希表

  1.哈希化 

  2.哈希函数 

  3.哈希表

二. 地址的冲突

三、链地址法(拉链法)

四、开放地址法

  1.线性探测

  2.二次探测

  3.再哈希法

五、哈希化的效率

  1.装填因子

  2.开放地址法

        (1).线性探测

        (2).二次探测和再哈希

  3.链地址法


一. 认识哈希表

        哈希表是一种非常重要的数据结构,几乎所有的编程语言都有直接或者间接的应用这种数据结构。

        哈希表通常是基于数组进行实现的, 但是相对于数组,,它有很多的优势它可以提供非常快速的插入-删除-查找操作。无论多少数据, 插入和删除值需要接近常量的时间:即O(1)的时间级。
哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。哈希表相对于树来说编码要容易很多。

        哈希表的结构就是数组,但是它神奇的地方在于对下标值的一种变换,这种变换我们可以称之为哈希函数,通过哈希函数可以获取到HashCode。


        哈希表相对于数组的一些不足:哈希表中的数据是没有顺序的,所以不能以一种固定的方式(比如从小到大)来遍历其中的元素。

        通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素。

 

  • 但是, 怎样才能将一个转成数组的下标值呢?

            单词转下标值,其实就是字母转数字,可以使用编码系统:
    • 比如ASCII编码:a是97,b是98,依次类推122代表z
    • 也可以设计一个自己的编码系统,比如a是1,b是2,c是3,依次类推,z是26。可以加上空格用0代替,就是27个字符(不考虑大写问题)
    • 计算机中有很多的编码方案就是用数字代替单词的字符。 
  • 方案一:幂的连乘

        平时使用的大于10的数字,可以用一种幂的连乘来表示它的唯一性:比如: 7654 = 7*10³+6*10²+5*10+4。就算能创建这么大的数组,事实上有很多是无效的单词。创建这么大的数组是没有意义的。这样得到的数字几乎可以保证它的唯一性,不会和别的单词重复。

        另外,单词也可以使用这种方案来表示: 比如cats = 3*27³+1*27²+20*27+17= 60337

  • 方案二:数字相加

        把单词每个字符的编码求和。例如单词cats转成数字:3+1+20+19=43,那么43就作为cats单词的下标存在数组中。这种方案的问题是很多单词最终的下标可能都是43。然而数组中一个下标值位置只能存储一个数据,如果存入后来的数据,必然会造成数据的覆盖。一个下标存储这么多单词显然是不合理的。

         第一种方案产生的数组下标太多,第二种又太少。

 

  1.哈希化 

        将大的数字转化成数组范围内下标的过程,称之为哈希化。

        假如有数字0~199,将每个数字取余,就能压缩为0~9的数字。

 

  2.哈希函数 

        将单词转成大数字大数字在进行哈希化的代码实现放在一个函数中,该函数称为哈希函数

 

  3.哈希表

        最终将数据插入到的这个数组,对整个结构的封装,称之为是一个哈希表

二. 地址的冲突

        如果有10000个单词,我们使用了20000个位置来存储并且通过一种相对比较好的哈希函数来完成,但是依然有可能会有两个或多个单词哈希化后的下标相同,此时就发生了冲突。为了解决冲突有两种方法:链地址法和开放地址法。

三、链地址法(拉链法)

 

        链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一个链条。

        这个链条使用的是数组或者链表。数组或链表在此处的效率差不多。因为根据哈希化的index找出这个数组或者链表时,通常就会使用线性查找,这个时候数组和链表的效率是差不多的。最好采用链表,因为数组在首位插入数据是需要所有其他项后移的,链表就没有这样的问题。

        如果链条使用的是链表,也就是每个数组单元中存储着一个链表。一旦发现重复,将重复的元素插入到链表的首端或者末端即可。当查询时, 先根据哈希化后的下标值找到对应的位置,再取出链表,依次查询找寻找的数据。

 四、开放地址法

        开放地址法的主要工作方式是寻找空白的单元格来放置冲突的数据项。探测这个位置的方式不同,有三种方法,分别是:线性探测、二次探测、再哈希法。

 

   1.线性探测

        线性探测非常好理解:线性的查找空白的单元

        (1).插入32

        经过哈希化得到的index=2,但是在插入的时候,该位置已经有了82。线性探测就是从index位置+1开始一点点查找合适的位置来放置32。空的位置就是合适的位置,在上例中就是index=3的位置,这个时候32就会放在该位置。

        (2).查询32

        首先经过哈希化得到index=2,比较2的位置的结果和要查询的数值是否相同,如果相同的话就直接返回。如果不相同就线性查找,从index位置+1开始查找和32一样的。

        在查询过程,查询到空位置,就停止。如果32的位置我们之前没有插入,查询到这里有空位置,就停止,因为32在之前插入时不可能跳过空位置去其他的位置

        (3).删除32

        删除操作一个数据项时,不可以将这个位置下标的内容设置为null。因为将它设置为null可能会影响之后查询其他操作,所以通常删除一个位置的数据项时,会将它进行特殊处理(比如设置为-1)。之后看到-1位置的数据项时,就知道查询时要继续查询,但是插入时这个位置可以放置数据.

 

  2.二次探测

        二次探测在线性探测的基础上进行了优化。二次探测主要优化的是探测时的步长

        线性探测,可以看成是步长为1的探测。比如从下标值x开始, 那么线性探测就是x+1, x+2, x+3依次探测。        

        二次探测对步长做了优化,比如从下标值x开始,x+1²,x+2²,x+3²。

        这样就可以一次性探测比较长的距离,避免聚集带来的影响。

 

  3.再哈希法

        二次探测的算法产生的探测序列步长是固定的:1, 4, 9, 16, 依次类推。现在需要一种方法: 产生一种依赖关键字的探测序列,而不是每个关键字都一样。

        那么,不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列。

        再哈希法的做法就是:把关键字用另外一个哈希函数,再做一次哈希化,用这次哈希化的结果作为步长。对于指定的关键字,步长在整个探测中是不变的,不过不同的关键字使用不同的步长。

        第二次哈希化需要具备如下特点:

  • 和第一个哈希函数不同。 (不要再使用上一次的哈希函数了,不然结果还是原来的位置)
  • 不能输出为0(否则将没有步长。每次探测都是原地踏步,算法就进入了死循环)

五、哈希化的效率

        哈希表中执行插入和搜索操作可以达到O(1)的时间级,如果没有发生冲突,只需要使用一次哈希函数和数组的引用,就可以插入一个新数据项或找到一个已经存在的数据项。

        如果发生冲突,存取时间就依赖后来的探测长度。一个单独的查找或插入时间与探测的长度成正比,这里还要加上哈希函数的常量时间。

        平均探测长度以及平均存取时间,取决于填装因子,随着填装因子变大,探测长度也越来越长。

  1.装填因子

        装填因子表示当前哈希表中已经包含的数据项和整个哈希表长度的比值。

                                                装填因子 = 总数据项 / 哈希表长度

        开放地址法的装填因子最大是1,因为它必须寻找到空白的单元才能将元素放入.

        链地址法的装填因子可以大于1,因为拉链法可以无限的延伸下去(但是后面效率就变低了)

 

  2.开放地址法

        (1).线性探测

        下面的等式显示了线性探测时,探测序列(P)和填装因子(L)的关系

                对成功的查找: P = (1+1/(1-L))/2

                对不成功的查找: P=(1+1/(1-L)^2)/2

        算法的效率:

 

        (2).二次探测和再哈希

        二次探测和再哈希法的性能相当。它们的性能比线性探测略好。

                        对成功的查找: -log2(1 - loadFactor) / loadFactor

                        对于不成功的查找:1 / (1-loadFactor)

        算法的效率:

 

  3.链地址法

        假如哈希表包含arraySize个数据项,每个数据项有一个链表,在表中一共包含N个数据项。那么, 平均起来每个链表有 N / arraySize 个数据。这个公式其实就是装填因子。

        成功可能只需要查找链表的一半: 1 + loadFactor/2

        不成功可能需要将整个链表查询完: 1 + loadFactor

        算法的效率:

故链地址法的效率是好于开放地址法的。所以在真实开发中,使用链地址法的情况较多,它不会因为添加了某元素后性能急剧下降。

举报

相关推荐

0 条评论