551-哈希表的理论讲解

哈希表的定义

在这里插入图片描述
哈希表增删查效率非常高,趋近于O(1)
没有办法达到绝对的O(1)
哈希表什么时候用到?在进行大量的查询时会用到,只要是有速度快的查询的需求,都会想到哈希表

我们想让这一组数据的查询的时间复杂度是O(1)

在这里插入图片描述
不管是存到链表还是数组里面,都是线性的查找,从第一个元素开始查,查一个比较一个,时间复杂度都是O(n)
最快的方法是提供一张表,还是数组, 我们在存的时候,并不是挨个存储,而是元素值的本身作为下标存到数组相应的位置

在这里插入图片描述
我们把数组的每个位置初始化为-1的值。然后把1,2,3,4,5,6,7,8分别存储在数组下标1,2,3,4,5,6,7,8的地方。
如果是这样存储的话,可能会浪费一些空间。比如说0号位置和7号位置的空间浪费了。这没有关系,关键是它可以提供一个时间复杂度是O(1)的查找的算法。
在这里插入图片描述
比如说,我现在要查找一个值为3的元素。我直接拿val这个值作为数组的下标去访问 ,如果值是-1,则这个元素3在数组中不存在,如果值不为-1,则3找到了。

在这里插入图片描述
元素的值通过一个映射关系(映射函数)找到元素在表中的存储位置
这张表就称为散列表(哈希表)
在这里插入图片描述
但是,上面的方式存储,空间会浪费,比如说是下面这些数字:
在这里插入图片描述
如果还是用刚才的映射关系,岂不是要开辟1000003个位置的内存空间?
只有4个位置存储有效元素,其他的那么多的空间位置都浪费了!

在实际的过程中,我们应该为了节省内存空间,找一个合适的映射关系!

常见的映射方法

在这里插入图片描述

常见的是除留余数法

假设我们有这么一组数据:
在这里插入图片描述

我们定义7个位置大小的数组空间。
元素的值作为输入参数,通过哈希函数处理之后,得到一个输出的值,就是在表中的存储位置,这个表就叫做哈希表或者散列表。
在这里插入图片描述
数组的每个位置都初始化为空
我们存储12的话怎么存?除留余数法,余上一个哈希表的长度
在这里插入图片描述
12就存在下标为5的位置。下标为5的位置是空的,所以把12存进去。

在这里插入图片描述
我们接下来要存储18
在这里插入图片描述
把18这个元素存储在哈希表下标为4的位置
在这里插入图片描述
我们接下来存储27
在这里插入图片描述
在这里插入图片描述
我们接下来存储24
在这里插入图片描述
在这里插入图片描述
我们接下来存储33
在这里插入图片描述
33存储的位置是哈希表下标为5的位置。但是现在哈希表下标为5的位置存储的是12,已经被占用了,而且数组下标这里就这一个位置,这种现象是产生哈希冲突(哈希碰撞)了,怎么办?
不管选择什么样的哈希函数,都会产生哈希冲突,是无法避免的,只能尽量减小哈希冲突产生的概率。
不同元素模上一个7很有可能得到的结果是相同的

解决哈希冲突的方案是什么?

在这里插入图片描述
unordered_set和unordered_map底层是用链式哈希表实现的,用链地址法解决哈希冲突

线性探测法:
就是往后继续探测,有空闲的位置就存放。
33经过除留余数法被分到哈希表的5号位置,但是5号位置已经有元素占着了,就把33往后走,找一个空闲的位置存放33
在这里插入图片描述

假如我们现在要存储45,在这里插入图片描述
但是3号位置也有元素占用了,往后走,后面的位置都有元素占着了,然后转回首地址往后走,发现1号位置空闲,就把45放到1号位置。(几乎是把数组遍历了一遍了----哈希冲突导致哈希表在存储元素的时候变慢了)
在这里插入图片描述
变得有多慢呢???
我们把一个元素按照哈希函数处理之后,放在某一个位置,这个处理是个常量的时间,哈希表在增加元素,时间复杂度是O(1)的,但是由于产生了哈希冲突,如果是用线性探测法,往后找空闲的位置,基本上是把整个哈希表数组遍历了,所以,哈希冲突越多,增加的元素的时间复杂度就从O(1)慢慢的变到了O(n)
在这里插入图片描述
现在我们要查询了
比如说,我们查询18,在这里插入图片描述
18进行哈希函数(除留余数法)得出是4,然后访问4号位置,是18,就查到了
为了在哈希表找18,先执行了一次哈希函数采用的除留余数法,是常量时间,然后进行哈希表数组的下标访问,也是一个常量时间 在这里插入图片描述
但是同样的,因为哈希冲突的存在,比如我要查45,先进行哈希函数的除留余数法,得出3,访问3号位置,发现3号位置上的元素是24而不是45,肯定当时往哈希表放45的时候已经产生哈希冲突了,所以45并没有在3号位置上放,我们还得知道这个哈希表解决哈希冲突的方案是什么:是线性探测法
就和环形队列一样,往后遍历,然后走环,最终在1号位置上找到了45,因为哈希冲突及线性探测法的存在,导致搜索的时间复杂度从O(1)–>O(n)

哈希表的删除元素的时间复杂度也是O(1),我们删除18,通过哈希表的除留余数法,得到4,4号位置就是18,所以就直接把18干掉了O(1)的操作
但是,如果我们要删除45,通过哈希表的除留余数法,得到3,发现3号位置不是45, 得知原来在插入45的时候产生了哈希冲突了,因为是线性探测法解决哈希冲突,又是环形的遍历,然后查到1号位置的元素是45,然后把45删掉。 在这里插入图片描述
在寻找45的时候发生了哈希冲突,导致删除的时间复杂度从O(1)–>O(n)
当我们在哈希表存放的元素越来越多,哈希冲突的发生概率也就越来越大
在增删查的时间复杂度从O(1)—>O(n)
实际上,我们会在多种情况下去优化,去降低哈希冲突的概率,但是是不能杜绝的。

降低哈希冲突的概率

方法1:我们在哈希函数上去想办法:
对于除留余数法来说,我们希望通过模上一个哈希表的长度后, 尽量产生的结果是离散的,而不是经常产生相同值导致哈希冲突 我们尽量让哈希表的长度(哈希桶的长度):取素数,只有1和自己能被整除,这样通除留余数产生的结果相同的概率就很低了
在C++的容器里,有专门定义一张素数表,在进行扩容的时候,采用的哈希函数是除留余数法的话,会采用素数 ,把所有的素数都枚举了一下
哈希表在扩容的时候,取的下一个长度就是在素数表里面的下一个素数

方法2哈希表的装载因子 loadfactor
装载因子:以占用位置(桶)的个数除以桶的总个数。
Java中或者C++链式哈希表实现的容器或者集合,装载因子值取的默认值是0.75或者四分之三

在这里插入图片描述

如果已占用桶的个数占到总的桶个数的四分之三的话,我们就说,此时这个哈希表放的元素已经比较多了,再继续放元素,产生的哈希冲突的概率就大 ,进而导致哈希表的增删查的时间复杂度变大,效率越来越低了。
此时如果是下面这个情况,哈希表就需要扩容了:
在这里插入图片描述
哈希表扩容以后,相当于数组的扩容,造价还是很大的,需要开辟更大长度的空间,采用素数的长度开辟更长的哈希表。
这时候,原来的元素就要放在新的哈希表里面了。

在这里插入图片描述
如果这个21原来放在旧的哈希表是0号位置,那么它在新的哈希表存放的也是一定在0号位置吗?
这个当然是不能确定的。
比如说当时哈希表的桶的个数是7,扩容之后,新的哈希表的桶的个数是23,那么除留余数法的模的数就不一样了,得到的值就不一定和原来一样了。
所以,对于哈希表的扩容来说,造价还是挺大的
在这里插入图片描述
扩容的时间复杂度是O(n)

哈希表的增删查时间复杂度:
在这里插入图片描述
因为并不是每次增加哈希表的元素都会触发哈希表的扩容,可能在第n次才触发到。

在这里插入图片描述
包括之间的vector的扩容,末尾元素增加,本身是O(1),但是如果末尾元素增加没有空间了, 导致数组需要先扩容了,扩容的话就是O(n),但是并不是每次末尾元素增加都会导致扩容啊,所以vector的末尾元素的增加的均摊时间复杂度是O(1)

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林林林ZEYU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值