序言:
HASH表对我来说是真没有听懂,尤其是构造HASN函数…
有同学也说再讲一下HASN那我也就把自学的另一点东西献出来吧。
HASH表
hash表主要是查找,对内存中的数据进行有效的快速查找它的查找时间复杂度是O(1)。
构造一个设计一个哈希表的关键有三个:怎么控制哈希表的长度,怎么设计哈希函数,怎么处理哈希冲突
怎样控制哈希表的长度
HASH表的长度一般是定长的,在存储数据之前我们应该知道我们存储的数据规模是多大,应该尽可能地避免频繁地让HASH表扩容。
但是如果设计的太大,那么就会浪费空间,因为我们跟不用不到那么大的空间来存储我们当前的数据规模;
如果设计的太小,那么就会很容易发生HASH冲突,体现不出HASH表的效率。
所以,我们设计的HASH表的大小,必须要做到尽可能地减小HASH冲突,并且也要尽可能地不浪费空间,选择合适的HASH表的大小是提升HASH表性能的关键。
当我们选择HASH函数的时候,经常会选择除留余数法,即用存储数据的key值除以HASH表的总长度,得到的余数就是它的HASH值。
常识告诉我们,当一个数除以一个素数的时候,会产生最分散的余数。由于我们通常使用表的大小对HASH函数的结果进行模运算,
如果表的大小是一个素数,那么这样我们就会尽可能地产生分散的HAHS值。
HASH表的另一个概念就是填装因子,它的公式和性质我就不再过多阐述。
至于为什么有这样一个概念,我的理解是,因为如果一个HASH表中的数据装的越多,是不是越容易发生HASH冲突。
如果当HASH表中满到只剩下一个下标可以插入的时候,这个时候我们还要往这个HASH表中插入数据,于是我们可能会达到一个O(n)级别的插入效率,
我们甚至要遍历整个HASH表才可能找到那个能存储的位置。
通常,我们关注的是使HASH表平均查找长度最小,把平均查找长度保证在O(1)的时间复杂度。
装填因子a的取值越小,产生冲突的机会就越小,但是也不能取太小,这样我们会造成较大的空间浪费。
如果我们a取 0.1 0.1 0.1,而HASH表的长度为 100 100 100,那我们只装了 10 10 10个键值 k e y key key对就存不下了,就要对HASH表进行扩容,而剩下90个键值 k e y key key对空间其实是浪费了的。
一般情况下,只要a取的合适(一般取0.7-0.8之间),哈希表的平均查找长度就会是常数也就是O(1)级别的。
总结一下,构造一个效率尽可能高的HASH表大小的方法:
·确保哈希表长度是一个素数,这样会产生最分散的余数,尽可能减少哈希冲突
·设计好哈希表装填因子,一般控制在0.7-0.8
·确认我们的数据规模,如果确认了数据规模,可以将数据规模除以装填因子,根据这个结果来寻找一个可行的哈希表大小
·当数据规模会动态变化,不确定的时候,这个时候我们也需要能够根据数据规模的变化来动态给我们的哈希表扩容,
所以一开始需要自己确定一个哈希表的大小作为基数,然后在此基础上达到装填因子规模时对哈希表进行扩容。
HASH函数(字符串HASH)
哈希函数,是用来计算存储数据的哈希值的,根据存储数据的类型,可以设计不同的哈希函数。
郭老师说构造了一个好的HASH函数可以减少冲突以及加快速度查找。。
一般的,字符串HASH一般都属于这个公式:
HASH值 = 计算后的存储值 / HASH表的大小
对于如果存储的数是整数这种类型,完全可以不用计算,直接将整数的值作为上式中计算后的存储值。
现在以字符串为例,来讲一下常见的字符串哈希算法,其他类型的数据都可以用相似的思路来设计适合自己的HASH算法。
就比如取一个固定值 P P P,把字符串看作 P P P的进制数,并分配一个大于0 的值,代表每种字符。一般来说,分配的数值都小于 P P P。
例如对于小写字母构成的字符串,最先想到应该是 A S C L L ASCLL ASCLL码,把这个小写字母的 A S C L L ASCLL ASCLL码赋值给它。
取一个固定值M,并求出这个P进制数对M的余数,作为这个字符串的HASH值
至于另外的构造HASH函数的方法看PPT就行了。
解决冲突的方法
前言:
陶立宇学长说的一句话,让我很震惊,见下:
为 什 么 要 解 决 H A S H 冲 突 , 不 解 决 它 只 会 慢 一 点 而 已 为什么要解决HASH冲突,不解决它只会慢一点而已 为什么要解决HASH冲突,不解决它只会慢一点而已
开发定址法:
这个办法就比较好理解,就是 H i Hi Hi不断循环,找到一个适合它的一个位置再装进去。
增量di可以有下面三种取值:
线性取值,1,2,3....这样,也就是从冲突位置不断往后找下一个可以存放的下标
二次取值,1,4,9....这样,也就是从冲突位置不断往后找x的二次方的下标,其中x从1开始线性增大
随机取值,di可以去任意随机值,随机找一个。
同样的这个办法比较浪费时间,而且不能删除元素。
再HASH法:
众所周知再这个字的意思,所以就是如果第一个HASH不能完全
解决冲突,那就再构造一个。
再HASH桶)—— 拉链法:(这里借鉴的思想且借张图)
每个下标中存的都是一个链表,相同哈希值的key直接往下标中的链表后面插入就行了
这种方法的特点是,表的大小和存储的数据数量差不多(大不了每个下标都只放一个节点,如果下标一样的都是放在同一下标的链表中,并没有占据新的下标) ,
因此哈希桶的方法没有特别依赖于装载因子,哈希表块满时,它还是可以做到较好的效率,而开发定址法就需要保证装载因子。
当然,哈希桶法并不是万能的,也有它的缺点:
它需要稍微多一点的空间来存放元素,因为还要有一个指向下一个节点的指针。
每次探测也要花费较多的时间,因为它需要间接引用指针,而不是直接访问元素。
但其实,对于上面的缺点,对于现在的电脑来说并不会有太大的影响,所以这些缺点是微不足道的,所以实际使用哈希时,一般都是用哈希桶来解决冲突。
需要注意的是,哈希表也不是能让这个链表无限长的。
打个比方,如果我所有的数据的哈希值都是一样,那么只会存在一个下标内,其余下标都没有用到,这就产生了一种极端情况。
在这种情况下,我们要查找起来,就等于在一个链表中查找,它的查找效率是O(n),这显然违背了哈希表设计的思维。
因此,在某一个链表或是几个链表的长度达到一定长度时,就需要对哈希表扩容,具体细节还是要看怎么设计了。
但是相比于开发定址法,它会发生扩容的频率就要小很多了,因为开发定址法还要保证自己的装填因子,所以它会更加频繁地扩容,
所以就效率而言,拉链法还是要优于开发定址法。