HASH(哈希表)学习

1. 什么是哈希表:

Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

2. 哈希表的优点:

就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间O(1);而代价仅仅是消耗比较多的内存。

3. 哈希表的基本原理:

使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数,也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标,hash值)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方,称为桶。

但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了“冲突”,换句话说,就是把不同的元素分在了相同的“类”之中。 总的来说,“直接定址”与“解决冲突”是哈希表的两大特点。

其插入过程是:

(1). 得到key

(2). 通过hash函数得到hash值

(3). 得到桶号(一般都为hash值对桶数求模)

(4). 存放key和value在桶内。

其取值过程是:

(1). 得到key

(2). 通过hash函数得到hash值

(3). 得到桶号(一般都为hash值对桶数求模)

(4). 比较桶的内部元素是否与key相等,若都不相等,则没有找到。

(5). 取出相等的记录的value。

4. 常用哈希算法:

(1)直接定址法 :地址集合 和 关键字集合大小相同

(2)数字分析法 :根据需要hash的 关键字的特点选择合适hash算法,尽量寻找每个关键字的 不同点

(3)平方取中法:取关键字平方之后的中间极为作为哈希地址,一个数平方之后中间几位数字与数的每一位都相关,取得位数由表长决定。比如:表长为512,=2^9,可以取平方之后中间9位二进制数作为哈希地址。

(4)折叠法:关键字位数很多,而且关键字中每一位上的数字分布大致均匀的时候,可以采用折叠法得到哈希地址,

(5)除留取余法除P取余,可以选P为质数,或者不含有小于20的质因子的合数

(6)随机数法:通常关键字不等的时候采用此法构造哈希函数较恰当。

实际工作中需要视不同的情况采用不同的hash函数:

(1)考虑因素:计算哈希函数所需要的时间,硬件指令等因素。

(2)关键字长度

(3)哈希表大小

(4)关键字分布情况

(5)记录查找的频率。(huffeman树)

元素特征转变为数组下标的方法就是散列法。散列法当然不止一种,下面列出三种比较常用的:

(1)除法散列法 

最直观的一种,上图使用的就是这种散列法,公式:index = value % 16

学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。

(2)平方散列法

求index是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。公式:  index = (value * value) >> 28 (右移,除以2^28。记法:左移变大,是乘。右移变小,是除。)

如果数值分配比较均匀的话这种方法能得到不错的结果,但我上面画的那个图的各个元素的值算出来的index都是0——非常失败。也许你还有个问题,value如果很大,value * value不会溢出吗?答案是会的,但我们这个乘法不关心溢出,因为我们根本不是为了获取相乘结果,而是为了获取index。

(3)斐波那契(Fibonacci)散列法

平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是拿value本身当作乘数呢?答案是肯定的。

(1)对于16位整数而言,这个乘数是40503 

(2)对于32位整数而言,这个乘数是2654435769

(3)对于64位整数而言,这个乘数是11400714819323198485

    这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946,…。另外,斐波那契数列的值和太阳系八大行星的轨道半径的比例出奇吻合。

    对我们常见的32位整数而言,公式:  index = (value * 2654435769) >> 28

    如果用这种斐波那契散列法的话,那上面的图就变成这样了:

clipboard

        注:用斐波那契散列法调整之后会比原来的取摸散列法好很多。

(4)"Timers33"算法

几乎所有的流行的hash map都采用了DJB hash function,俗称“Times33”算法。

times33的算法也很简单,就是不断的乘33。nHash = nHash*33 + *key++;

经典times33算法如下:

inline UINT CMyMap::HashKey(LPCTSTR key) const

{

    UINT nHash = 0;

    while (*key)

        nHash = (nHash<<5) + nHash + *key++;

    return nHash;

}

5. 解决冲突的两种方法:

(1)分离链接: 就是将数据实际存放在与 hash 表存储单元相链接的链表中,而不是 hash 的存储单元中。、

clipboard[1]

       当产生冲突的时候,将两个数据都链接在同一 hash 存储单元保存的链表中。当一个存储单元保存的链表中有多个数据的时候,对于链表后面的数据的查找添加和删除就是不是严格意义上的 O(1) 了。一个好的 hash 函数可以使得这个链表很短。最坏情况下,当所有的数据都保存在一个 hash 单元指定的链表中的时候,那么这个 hash 就和链表一样了。

(2)开放地址: 使用开放地址方法解决冲突的时候,数据仍然保存在 hash 表的存储单元中,但是当冲突发生的时候,要再次计算新的地址。常用的开放地址法是线性探查,就是当对一个数据进行插入删除或者查找的时候,通过 hash 函数计算,发现这个位置不是要找的数据,这时候就检查下一个存储单元,一直找到要操作的数据为止。 除了线性探查外还有二次探查,再 hash 等等方法,都是当一次计算得到的位置不是要找到的数据的时候,怎样再次确定新的位置。

6. hash使用实例

工作中使用中见过两种方法:

(1)特定的数据 % 整个size。

(2)使用斐波那契(Fibonacci)散列法,即对我们常见的32位整数而言,公式:  index = (value * 2654435769) >> 28

发布了16 篇原创文章 · 获赞 5 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/ok_wolf/article/details/41451143