浅谈哈希表与其映射函数(哈希函数)

哈希表又称散列表,通过把关键字key映射到数组中的一个位置来访问记录。映射过程通过函数实现,而这个函数就叫哈希函数,存放关键字的数组称为散列表。

哈希表结构

这里写图片描述

前面说了,关键字是存放在数组中的,所以哈希表的结构其实就是一个数组,为什么要采用数组来作为哈希表的数据结构呢?这里我不得不说数组的一些特性。
数组的时间复杂度是O(1),这里说的时间复杂度是访问复杂度,不是遍历复杂度。计算机内存被设定为直接访问任意一个地址的时间是一致的,结合该特性,我们知道数组在内存中的存储是,数组名存放在栈内存中,保存数组第一个元素的地址,而数组本身存放在堆内存中,其存储空间是连续的,所以我们只需要知道数组名就可以非常快速的定位数组任意位置。

哈希算法

哈希算法的作用是将关键字通过一系列计算,得出的结果作为数组下标,然后再将关键字存放到该下标对应的位置。哈希算法是决定哈希表中元素排列结构的最主要因素,不同的哈希算法会导致同样的数据出现不同的存储顺序。针对不同类型的关键字哈希算法也不同。下面是针对整数关键字的几种哈希算法(如果关键字是字符串,也可将字符串所有字符的ASCII码加起来得到一个整数再进行计算):
直接取余:顾名思义,直接取余法是用关键字除以一个固定值取余数,一般这个固定值取哈希表的大小。如果一个哈希表的大小是16,那么关键字100的存放位置应该是数组下标为4的位置。

乘积取整:成绩取整法是用关键字乘以一个常数A(0<A<1),然后取乘积的小数部分再与哈希表的大小求积,最后取结果的整数部分。同上,若哈希表的大小为16,A取0.025,那么关键字100的存放位置应该是数组下标为8的位置。
上面提到的两种算法都非常简单,但都存在很大的问题,这两种算法在某种程度上都会产生大量的冲突,即不同的数通过算法得出的结果相同,这样会令哈希的效果大打折扣。下面介绍一种解决冲突非常有效的哈希算法,经典哈希算法Time33

uint32_t time33(char const *str, int len) 
    { 
        unsigned long  hash = 0; 
        for (int i = 0; i < len; i++) { 
            hash = hash *33 + (unsigned long) str[i]; 
        } 
        return (hash & 0x7FFFFFFF); 
    }

从代码中我们可以看出,这种算法就是将关键字每一位拿出来,逐个相加,每相加一次都乘以33,最终获得我们要的结果。该算法可最大程度防止冲突的发生,亦可避免字符串类型的关键字顺序不同而所含字符相同导致计算结果相同(如adcfg和dcfga)。在php中,一个字符串长度过长不好计算,我们可以将其先利用MD5加密转化为32位的字符串,然后再对这个字符串进行计算即可。

在学习Time33之前,我自己也想过一个避免该类问题的算法,供大家讨论,思想如下:
这里写图片描述
如图,针对一个关键字,我们可对其从1开始进行依次编号,然后取每一位的值或者ASCII码值与它所对应的编号相乘,最后求和。这种方法与Times33类似,都可以有效的解决大量冲突问题,但是仍避免不了一些及特殊的情况。为了防止这些哈希算法无法避免的冲突,所以人们开始从哈希表的结构下手,希望通过改变哈希表的结构来避免这些冲突,因此又出现了许多避免冲突的方法,如“拉链法”解决冲突。

“拉链法”解决冲突的做法是将所有哈希值相同的关键字节点链接在同一个链表中。如下图:

这里写图片描述

“拉链法”将哈希值相同的关键字通过链表的方式连接起来,确实有效的解决了冲突问题,但是在查询关键字的时候,若所查询的位置无冲突,那么查询的时间复杂度为O(1),但如果所查询的位置出现冲突,就需要进一步遍历链表去查询,这样的话查询的效率大打折扣,时间复杂度并不能满足所谓的O(1),所以说哈希表只能在理想无冲突的情况下,时间复杂度才能到达O(1)。因此来看,要降低时间复杂度最终的瓶颈还是在怎么防止冲突的问题上而不是出现冲突怎样处理的问题上。但是并不产生冲突几乎是不可能实现的,那么有没有一种办法既能有效处理出现的冲突,又能优化时间复杂度呢?我不知道现在有没有这种方法,但是我提供一种思路供大家思考,这种思路叫“二次散列”。

“二次散列”,说白了就是对每一个关键字都进行两种不同形式/函数的散列,然后根据两次的结果确定该关键字的位置(如果两个不同的关键字通过哈希函数A算出的哈希值相等,那么他们通过哈希函数B所算出的哈希值几乎不可能相等,哈希函数A和B可自行决定,但是最好取两种散列思路不同的算法)。而存储方式就叫“坐标法”,跟”拉链法”大体相似,不同的是,”拉链法”采用链表储存冲突,“坐标法”利用数组存储冲突。该结构的特点是它有一个纵向数组,类似于哈希表的原始结构,纵向数组每一位对应一个横向数组(横向数组命名应为编号式,下划线后的数字为所对应的纵向数组下标,如M_0[]、M_1[]….),第一次散列所计算出哈希值为纵向数组下标,若该位置为空说明无冲突发生,直接存储;若有值,判断是否相等,若不等说明冲突,先为该下标申请其所对应的横向数组,然后进行第二次散列,第二次散列所对应的哈希值为横向数组的下标,也就是该关键字所要存放的位置,类似于X/Y轴,查询的时候最多只需做两次散列一次比较,就可以确定关键字的具体位置。为什么不直接使用二维数组呢?因为毕竟不是所有位置都会发生,使用二维数组会过度消耗内存空间。该方法还是存在很多缺陷,只提供一个思考的方向。

总的来说,散列函数的好坏是决定哈希表性能的关键,一个好的哈希函数一定具备两个特性,一是足够聚集,二是不重叠,这样即保证了内存的有效利用也保证了查询的时效,但这两个特性在理论上相悖,只能去找一个平衡点,使性能最大化。

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Jeaforea/article/details/79653664
文章标签: hash
个人分类: 算法 hash
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭