javascript数据结构 -- 哈希表(二)

212 阅读5分钟

哈希表

本文主要说明处理下标值冲突的两种方法:链地址法和开放地址法;重点介绍开发地址法中的三种方法:线性探索法、二次探索法、再哈希法

1. 链地址法(拉链法)

原理

  • 链地址法处理下标冲突的方法,简单来说就是:不处理
  • 下标重复也没有什么关系,只要此下标对应的内容是一个①线性结构即可,冲突的内容就可以无限的push进来
  • 如下图所示:
[0] -> ['a', 'b', ..., 'm']
[1] -> ['k', 'h']
[2] -> ['e']
[.] -> [...]
[.] -> [...]
[.] -> [...]
[n] -> ['y']

线性结构是什么

  • 数组
  • 链表 实际上都是可以的,不论是数组还是链表,在根据内容或者内容的一部分进行索引的时候效率是差不多的! 但是链表在插入和删除方面具有优势,所以个人比较倾向于使用链表

2. 开放地址法

原理

  • 开发地址法处理下标冲突的思路简而言之就是: 退而求其次和退退而求其次次
  • 主打的就是一个随心写意,此处不留爷自有留爷处
  • 就是说,一旦发现下标重复了,就为新来的重新找一个去处
  • 这种重新找一个去处的机制有三种:
    • 线性探测
    • 二次探测
    • 再哈希法

2.1 线性探测方法

假设对37取余之后,得到的下标如下所示,其中0表示没有被占有,1表示被占有:

[0] -> [0] -> []
[1] -> [0] -> []
[2] -> [1] -> ['uuid']
[3] -> [1] -> ['ttbb']
[4] -> [1] -> ['xyz]
[5] -> [0] -> []
[6] -> [1] -> ['okk']

假如现在字符串'zsli'经过哈希函数转化,然后取余之后得到下标值为: 2 这个2往上面的数组中插入的时候发现下标2已经被占用了,这个时候就去找2的下一个,也就是下标3是不是空的,然后发现下标3也被占用了 有点尴尬,这个时候回去找4,然后是5,发现下标5空着,于是就将内容填进去:

[0] -> [0] -> []
[1] -> [0] -> []
[2] -> [1] -> ['uuid']
[3] -> [1] -> ['ttbb']
[4] -> [1] -> ['xyz]
[5] -> [1] -> ['zsli']
[6] -> [1] -> ['okk]

于是形成如下的映射关系: 'zsli' -> 2 -> 5' 既然如此, 应该如何通过'zsli'找到其存储的下标5呢?

  • 首先,字符串'zsli'经过哈希函数转化,然后取余之后得到下标值为: 2,然后取出下标2中的内容'uuid',发现不等于'zsli'
  • 那就再取出下标3中的内容'ttbb',发现还不是'zsli';没有关系,继续往下找,直到取出下标5的内容为'zsli',这个时候就找到了索引值为:5
  • 有没有感觉后半段的做法很蠢?这个也算效率高?
  • 实际上,这个过程和下标2,3,4都冲突了,冲突了3次,所以感觉效率低,那么如果能够减少冲突的次数,效率不就上去了吗?
  • 这种连续的下标冲突有一个专门的术语:聚集,聚集是线性探测方法的固有问题,此方法可以通过二次探测方法解决

注意: 使用线性探测方法最终得到的哈希表在删除元素的时候不能将其直接置空,而是将其中的内容置为-1,以表示此位置已经被占用过;

这个需要想明白,因为线性探测是找到第一个空白的位置插入新来的,所以如果删除是将某个位置置为空,则会导致后续查找的错误!

小结一下:所谓的线性探测体现在探测步长是线性增加的,用数列表示就是:a(n) = n*(1);

2.2 二次探测方法

主要是为了缓解线性探测方法造成的聚集问题;

原理: 不同于2, 3, 4, 5这样的查找次序,二次检测方法查找过程为: 2, 2+1^2, 2+2^2, 2+3^2 ... 也就是2, 3, 6, 11 ...

本质并没有改变,在遇到特殊的情况时,聚集仍然存在,所以说只能是缓解;

彻底解决这个问题需要用到再哈希法

小结一下:所谓的线性探测体现在探测步长是线性增加的,用数列表示就是:a(n) = n*(n);

2.3 再哈希方法

线性探测方法和二次探测方法无法解决聚集问题的本质在于: 不论是1 2 3 4 还是 1 4 9 16, 都是固定的检测步长序列

而解决聚集问题的核心在于, 就是'zsli'是一个序列: 1 2 3 4, 'uuid'是另外的序列: 1 4 9 16

那么如何实现呢?

原理: a(n) = n * (constant - (key % constant)); 注意: 由于key % constant恒小于constant,所以constant - (key % constant)恒大于0;

其中的key来自于另外一个哈希函数g,此函数根据内容产生另外一个数字,只不过为了保证key % constant恒小于constant,所以key绝对不能为0, 也就是说g映射之后的结果不会为0;

注意:再哈希法根据不同的字符串产生不同的步长, 但是constant - (key % constant)是定值,是定值,是定值!

2.4 优秀的哈希函数

不难看出来,如果使用再哈希法,那么哈希表的实现过程中就会用到两次哈希函数,那么如此重要的哈希函数必须是经过精心设计的才对.

自然就会出现一个问题,那就是:什么才是优秀的哈希函数?

  • 高效: 从字符串到数字的计算过程要尽可能的快

  • 均匀: 映射产生的数字应该尽可能的均匀分布,极端情况就是:1. 产生的数字毫无规律,跨度很大; 2. 产生的数字过于集中,导致重复度很高;

  • 高效之路: 在计算过程中尽可能的减少乘法/除法这些高消耗算法的次数

  • 均匀之路: 哈希表的长度需要取质数,否则探测会有问题:假如表的长度为15,而步长为5,则探测序列就是: x, x+5, x+10, x+15, ...

  • 发现了没有x和x+15是一样的!实际上就死循环了!

在链地址方法中,哈希表的大小是不是质数不重要,比如java中哈希表的长度就是2^N,也就是每次扩容,容量简单翻倍.