哈希表的概念理解

1.什么是Hash表?算法

        Hash表也称散列表,也有直接称为哈希表,是一种根据关键字值(key-value)而直接进行访问的数据结构。它是经过把关键字映射到数组的下标来加快查找速度。普通的数据结构中查找某一个关键字一般须要遍历整个数据结构,时间复杂度O(n),而哈希表只须要O(1)的时间级。数组

       咱们知道个重要的问题就是如何把关键字转换为数组的下标,这个转换的函数称为哈希函数(也称散列函数),转换的过程称为哈希化。数据结构

2.介绍哈希函数函数

       你们都用过字典,字典的优势是咱们能够经过前面的目录快速定位到所要查找的单词。若是咱们想把一本英文字典的每一个单词,从 a 到 zyzzyva(这是牛津字典的最后一个单词),都写入计算机内存,以便快速读写,那么哈希表是个不错的选择。编码

  这里咱们将范围缩小点,好比想在内存中存储5000个英文单词。咱们可能想到每一个单词会占用一个数组单元,那么数组的大小是5000,同时能够用数组下标存取单词,这样设想很完美,可是数组下标和单词怎么创建联系呢?设计

  首先咱们要创建单词和数字(数组下标)的关系:内存

  咱们知道 ASCII 是一种编码,其中 a 表示97,b表示98,以此类推,一直到122表示z,而每一个单词都是由这26个字母组成,咱们能够不用 ASCII 编码那么大的数字,本身设计一套相似 ASCII的编码,好比a表示1,b表示2,依次类推,z表示26,那么表示方法咱们就知道了。开发

  接下来如何把单个字母的数字组合成表明整个单词的数字呢?变量

  ①、把数字相加扩展

  首先第一种简单的方法就是把单词的每一个字母表示的数字相加,获得的和即是数组的下标。

  好比单词 cats 转换成数字:

  cats = 3 + 1 + 20 + 19 = 43

  那么单词 cats 存储在数组中的下标为43,全部的英文单词均可以用这个办法转换成数组下标。可是这个办法真的可行吗?

  假设咱们约定一个单词最多有 10 个字母,那么字典的最后一个单词为 zzzzzzzzzz ,其转换为数字:

  zzzzzzzzzz = 26*10 = 260

  那么咱们能够获得单词编码的范围是从1-260。很显然,这个范围是不够存储5000个单词的,那么确定有一个位置存储了多个单词,每一个数组的数据项平均要存储192个单词(5000除以260)。

  对于上面的问题,咱们如何解决呢?

  第一种方法:考虑每一个数组项包含一个子数组或者一个子链表,这个办法存数据项确实很快,可是若是咱们想要从192个单词中查找到其中一个,那么仍是很慢。

  第二种方法:为啥要让那么多单词占据同一个数据项呢?也就是说咱们没有把单词分的足够开,数组能表示的元素太少,咱们须要扩展数组的下标,使其每一个位置都只存放一个单词。

  对于上面的第二种方法,问题产生了,咱们如何扩展数组的下标呢?

  ②、幂的连乘

  咱们将单词表示的数拆成数列,用适当的 27 的幂乘以这些位数(由于有26个可能的字符,以及空格,一共27个),而后把乘积相加,这样就得出了每一个单词独一无二的数字。

  好比把单词cats 转换为数字:

  cats = 3*273 + 1*272 + 20*271 + 19*270 = 59049 + 729 + 540 + 19 = 60337

  这个过程会为每一个单词建立一个独一无二的数,可是注意的是咱们这里只是计算了 4 个字母组成的单词,若是单词很长,好比最长的10个字母的单词 zzzzzzzzzz,仅仅是279 结果就超出了7000000000000,这个结果是很巨大的,在实际内存中,根本不可能为一个数组分配这么大的空间。

  因此这个方案的问题就是虽然为每一个单词都分配了独一无二的下标,可是只有一小部分存放了单词,很大一部分都是空着的。那么如今就须要一种方法,把数位幂的连乘系统中获得的巨大的整数范围压缩到可接受的数组范围中。

  对于英语字典,假设只有5000个单词,这里咱们选定容量为10000 的数组空间来存放(后面会介绍为啥须要多出一倍的空间)。那么咱们就须要将从 0 到超过 7000000000000 的范围,压缩到从0到10000的范围。

  第一种方法:取余,获得一个数被另外一个整数除后的余数。首先咱们假设要把从0-199的数字(用largeNumber表示),压缩为从0-9的数字(用smallNumber表示),后者有10个数,因此变量smallRange 的值为10,这个转换的表达式为:

  smallNumber = largeNumber % smallRange

  当一个数被 10 整除时,余数必定在0-9之间,这样,咱们就把从0-199的数压缩为从0-9的数,压缩率为 20 :1。

  咱们也能够用相似的方法把表示单词惟一的数压缩成数组的下标:

  arrayIndex = largerNumber % smallRange

      这也就是哈希函数。它把一个大范围的数字哈希(转化)成一个小范围的数字,这个小范围的数对应着数组的下标。使用哈希函数向数组插入数据后,这个数组就是哈希表。

3.冲突问题

      将大范围的数字范围压缩到较小的数字中,确定会有几个不一样的单词哈希化到同一个数字下标,即产生冲突。

      解决冲突的方法有两种

      第一是开放地址法,即当冲突产生时,而经过系统的方法找到数组的一个空位,并将这个单词填入,而再也不用哈希函数获得数组的下标。

     第二是链地址法,就是数组的每个数据项都建立一个子链表或子数组,那么数组内不直接存储单词,当冲突产生时,新的数据项直接存放到这个数组下标表示的链表中,这种方法称为链地址法。

 4.开放地址法

开发地址法中,若数据项不能直接存放在由哈希函数所计算出来的数组下标时,就要寻找其余的位置。分别有三种方法:线性探测、二次探测以及再哈希法。

①、线性探测

  在线性探测中,它会线性的查找空白单元。好比若是 5421 是要插入数据的位置,可是它已经被占用了,那么就使用5422,若是5422也被占用了,那么使用5423,以此类推,数组下标依次递增,直到找到空白的位置。这就叫作线性探测,由于它沿着数组下标一步一步顺序的查找空白单元。

     须要注意的是,当哈希表变得太满时,咱们须要扩展数组,可是须要注意的是,数据项不能放到新数组中和老数组相同的位置,而是要根据数组大小从新计算插入位置。这是一个比较耗时的过程,因此通常咱们要肯定数据的范围,给定好数组的大小,而再也不扩容。

  另外,当哈希表变得比较满时,咱们每插入一个新的数据,都要频繁的探测插入位置,由于可能不少位置都被前面插入的数据所占用了,这称为汇集。数组填的越满,汇集越可能发生。

  这就像人群,当某我的在商场晕倒时,人群就会慢慢汇集。最初的人群聚过来是由于看到了那个倒下的人,然后面聚过来的人是由于它们想知道这些人聚在一块儿看什么。人群汇集的越大,吸引的人就会越多。

 

②、装填因子

  已填入哈希表的数据项和表长的比率叫作装填因子,好比有10000个单元的哈希表填入了6667 个数据后,其装填因子为 2/3。当装填因子不太大时,汇集分布的比较连贯,而装填因子比较大时,则汇集发生的很大了。

 

  咱们知道线性探测是一步一步的日后面探测,当装填因子比较大时,会频繁的产生汇集,那么若是咱们探测比较大的单元,而不是一步一步的探测呢,这就是下面要讲的二次探测。

③、二次探测

     二测探测是防止汇集产生的一种方式,思想是探测相距较远的单元,而不是和原始位置相邻的单元。

  线性探测中,若是哈希函数计算的原始下标是x, 线性探测就是x+1, x+2, x+3, 以此类推;而在二次探测中,探测的过程是x+1, x+4, x+9, x+16,以此类推,到原始位置的距离是步数的平方。二次探测虽然消除了原始的汇集问题,可是产生了另外一种更细的汇集问题,叫二次汇集:好比讲184,302,420和544依次插入表中,它们的映射都是7,那么302须要以1为步长探测,420须要以4为步长探测, 544须要以9为步长探测。只要有一项其关键字映射到7,就须要更长步长的探测,这个现象叫作二次汇集。二次汇集不是一个严重的问题,可是二次探测不会常用,由于还有好的解决方法,好比再哈希法。

 ④、再哈希法

  为了消除原始汇集和二次汇集,咱们使用另一种方法:再哈希法。

  咱们知道二次汇集的缘由是,二测探测的算法产生的探测序列步长老是固定的:1,4,9,16以此类推。那么咱们想到的是须要产生一种依赖关键字的探测序列,而不是每一个关键字都同样,那么,不一样的关键字即便映射到相同的数组下标,也可使用不一样的探测序列。

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

  第二个哈希函数必须具有以下特色:

  1、和第一个哈希函数不一样

  2、不能输出0(不然,将没有步长,每次探测都是原地踏步,算法将陷入死循环)。

  专家们已经发现下面形式的哈希函数工做的很是好:stepSize = constant - key % constant; 其中constant是质数,且小于数组容量。

再哈希法要求表的容量是一个质数,假如表长度为15(0-14),非质数,有一个特定关键字映射到0,步长为5,则探测序列是0,5,10,0,5,10,以此类推一直循环下去。算法只尝试这三个单元,因此不可能找到某些空白单元,最终算法致使崩溃。若是数组容量为13, 质数,探测序列最终会访问全部单元。即0,5,10,2,7,12,4,9,1,6,11,3,一直下去,只要表中有一个空位,就能够探测到它。

5.链地址法

       链地址法中,装填因子(数据项数和哈希表容量的比值)与开放地址法不一样,在链地址法中,须要有N个单元的数组中转入N个或更多的数据项,所以装填因子通常为1,或比1大(有可能某些位置包含的链表中包含两个或两个以上的数据项)。

找到初始单元须要O(1)的时间级别,而搜索链表的时间与M成正比,M为链表包含的平均项数,即O(M)的时间级别。

相关文章
相关标签/搜索