在OvS查表的过程中,频繁的使用到Hash算法,本文主要介绍Hash算法的功能和原理。
1 Hash算法简介
哈希查找算法又称散列查找算法,是一种借助哈希表(散列表)查找目标元素的方法。哈希表(Hash table)又称散列表,是一种存储结构,通常用来存储多个元素。每个存储到哈希表中的元素,都配有一个唯一的标识(又称“索引”或者“键”),用户想查找哪个元素,凭借该元素对应的标识就可以直接找到它,无需遍历整个哈希表。
多数场景中,哈希表是在数组的基础上构建的。使用数组构建哈希表,最大的好处在于:可以直接将数组下标当作已存储元素的索引,不再需要为每个元素手动配置索引,极大得简化了构建哈希表的难度。在数组中查找一个元素,除非提前知晓它存储位置处的下标,否则只能遍历整个数组。哈希表的解决方案是:各个元素并不从数组的起始位置依次存储,它们的存储位置由专门设计的函数计算得出,我们通常将这样的函数称为哈希函数。
2 Hash算法原理
哈希函数类似于数学中的一次函数,我们给它传递一个元素,它反馈给我们一个结果值,这个值就是该元素对应的索引,也就是存储到哈希表中的位置。举个例子,将{10, 20, 30, 50, 60}存储到哈希表中,我们设计的哈希函数为 y=x/10,最终各个元素的存储位置如下图所示。在下图的基础上,假设我们想查找元素50,只需将它带入y=x/10这个哈希函数中,计算出它对应的索引值为5,直接可以在数组中找到它。借助哈希函数,我们提高了数组中数据的查找效率,这就是哈希表存储结构。
构建哈希表时,哈希函数的设计至关重要。假设将{5, 20, 30, 33, 55}存储到哈希表中,哈希函数是y=x%10,各个元素在数组中的存储位置如下图所示。可以看到,5和55、20和30对应的索引值是相同的,它们的存储位置发生了冲突,我们习惯称为哈希冲突或者哈希碰撞。设计一个好的哈希函数,可以降低哈希冲突的出现次数。哈希表提供了很多解决哈希冲突的方案,比如线性探测法、链地址法、再哈希法等。
3 Hash冲突解决方案
3.1 线性探测法
如果使用线性探测法解决哈希冲突,解决方法是:当元素的索引值(存储位置)发生冲突时,从当前位置向后查找,直至找到一个空闲位置,作为冲突元素的存储位置,则上图所示的存储关系变换为下图所示。假设我们从下图所示的哈希表中查找元素30,查找过程需要经过以下几步:
- 根据哈希函数y=x%10,目标元素的存储位置为0,但经过和下标为0处的元素20比较,该位置存储的并非目标元素。
- 根据线性探测法,比较下标位置为1处的元素30,二者相等,成功找到目标元素。
3.2 链地址法
链表地址法是使用一个链表数组,来存储相应数据,当hash遇到冲突的时候依次添加到链表中。链地址在处理的流程如下:
- 添加一个元素的时候,首先计算元素key的hash值,确定插入数组中的位置。
- 如果当前位置下没有重复数据,则直接添加到当前位置。
- 当遇到冲突的时候,添加到同一个hash值的链表中,这个链表的特点是同一个链表上的hash值相同。
OvS中便是使用链地址法来解决hash冲突,key-value的索引存储在hash表中,如果多个key映射到同一hash table bucket中,通过链表的形式连接起来。每个hash链表以first作为链表的头,hash链表是由多个以first作为头的链表组成。每个hlist_node的next成员保存的是下一个hlist_node的地址,hlist_node的pprev成员保存的是前一个hlist_node的地址。如果是链表第一个元素,保存的是first的地址,最后一个hlist_node的next成员的内容是null,也就是指向null。新增的key-value项插入到链表的头部。
3.3 再Hash法
再Hash法同时构造多个不同的哈希函数,如h1=x%10,h2=x%7。当使用h1发生冲突时,再用h2进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。
3.4 布谷鸟Hash算法
布谷鸟Hash算法引入kick的概念来解决Hash冲突。在该算法中,定义了两个hash table,同时定义了两个hash函数h1和h2。每个key可以存储在table 1的h1(x)槽或table 2的h2(x)槽中,插入步骤如下:
- 在插入key x时,查询table 1的h1(x)槽是否已被占据。
- 如果没有,则插入,结束任务;
- 如被占据,仍然将x插入该位置,将原来的y插入T2的h2(y)中。
- 如此循环,直至所以的key都插入指定位置。
- 在插入key的过程中可能出现loop,因此存在一个最大循环次数。