掌握哈希表

Posted 你这家伙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了掌握哈希表相关的知识,希望对你有一定的参考价值。

首先来看一个问题?
:在[5,1,4,7,6,5,7,1,6],在这组数据中查找某一个元素有几种方法呢?

:①暴力可以的话,那就可以直接遍历数组嘛;②咱们也可以先把他排个序,然后在使用前面学的二分查找呀;③实在不行,我们可以将他构建成一颗二叉搜索树呀……

:害,你这些都可以,但是你直接遍历的话,时间复杂度就位O(n)了呀,二分查找的时间复杂度为O(logn),二叉搜索树的时间复杂度为O( l o g 2 N log_2 N log2N)了,有没有时间复杂度更低的呀,像O(1)这样的时间复杂度~

:我……

:算了,我来跟你说一种吧,它就是我们常用的哈希表……

1. 概念

散列表(Hashtable,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。(哈希表底层是一个数组,而且用的就是除留余数法,是典型的空间换取时间)

哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash
Table)(或者称散列表)
例如:数据集合1,7,6,4,5,9;
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小

  • 插入元素
    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素
    对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

2. 哈希冲突

对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) == Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

2.1 如何避免哈希冲突呢?

首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率。

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。哈希函数设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1 之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常见的哈希函数

  1. 直接定制法
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且连续的情况

  2. 除留余数法
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

2.2 冲突-避免-负载因子调节

负载因子 = 填入表中的元素 / 哈希表(散列表)的长度

我们不难发现,负载因子越大,冲突率就越大;负载因子越小,冲突率越小
但是负载因子 = 插入元素个数 / 散列表的长度,而插入元素的个数是变不了了,那我们就从散列表的长度下手

所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。

3. 冲突-解决-闭散列

3.1 线性探测法

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去

比如:现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

线性探测(就是从当前发生冲突的位置向后探测,直到找到下一个位置,以为在放的同时我们也有负载因子的调节,所以不用担心满的情况)但是此方法有个不好的地方,不能随意的在哈希表中物理的删除某个元素,那么导致误判,会导致后面与要删除元素的元素找不到了,此时就需要标记,表示这个位置我删过,还要一个不好的的地方,就是冲突的元素放在一起了

3.2 二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m,或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。其中:i = 1,2,3…, H 0 H_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。对于2.1中如果要插入44,产生冲突,使用解决后的情况为:

就是如果当前位置冲突了,就放在“当前位置+i^2”(i表示第几次冲突),当i=1的时候,当去放元素的时候,发现当前位置有元素,那么此时就计算i=2时的位置,如果此时超出哈希表的长度的时候就把它当成一个循环,然后接着从0号位置开始寻找,(既%上一个哈希表的长度)
这个方法优点在于每次能够将冲突的元素均匀的分布

3.4 冲突-解决-开散列/哈希桶

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

冲突严重时的解决办法

刚才我们提到了,哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:

  1. 每个桶的背后是另一个哈希表
  2. 每个桶的背后是一棵搜索树

以上是关于掌握哈希表的主要内容,如果未能解决你的问题,请参考以下文章

哈希表/散列表

Python数据结构-哈希表(Hash Table)

哈希查找

LeetCode 166 分数到小数[字符串 哈希表] HERODING的LeetCode之路

[数据结构] 哈希表 (开放寻址法+拉链法)

20162330 第十二周 蓝墨云班课 hash