一起了解数据结构-hash

前言

假如给你上万数据,然后让你判断,这些数据中是否有某一数据A?该如何进行呢?如果是一一进行判断,就显得很慢。如果是先将其有序化,然后再进行判断的话,判断速度倒是快了很多,但是排序这一过程也是会消耗时间的。
这时候,哈希结构就出现了。它能让你一次(理想情况下)完成判断,即理想情况下时间复杂度为O(1)。

hash表

哈希表其实就是key与value的对应关系,本质依托于哈希函数。将key输入到hash函数中,输出一个哈希后的结果,此结果作为存放value的依据。
一个好的哈希函数应该具有以下特点:
1.对于不同的输入,应当有不同的输出;(尽可能少的哈希冲突)
2.对于输入的微小变化,应当在输出端有较大变化;
常见的哈希函数有除法哈希,斐波那契哈希等。(菲波那切哈希简言之就是根据key的不同长度,乘上一个特定的数,然后最终结果左移28bit,至于这些特定的数,听说和黄金分割数有很大的关系)

hash冲突

哈希冲突其实是很难避免的,比如我的哈希函数是对10取模,那么当key输入是1和11和21,其实对应的value都是一样的,为1.那么我们该如何解决哈希冲突呢?

链地址法

其实很好理解,我放张图就好了。
在这里插入图片描述
那么当遇到key经过哈希后得到的value有不少情况时,就按照链表查下去即可。

开放地址法

开放地址法其实也很容易理解,即:通过哈希将key计算得到对应的值,后发现此数已经存在于数组中,则可以通过顺延的方式将数据插入到下一个位置。
线性探测再散列:在当前位置做如下偏移:1,-1,2,-2,3,-3…
二次探测再散列:sqrt(1), -sqrt(1),sqrt(2),-sqrt(2)…
开放地址法的删除可就有点讲究了,可不是那么随便就直接删除了。因为如此一来,咱们探测的路,可能就断了。因此咱们的处理都是在这个要删除的地方做一个标记,表示这是已经被删除的。再探测的时候呢,如果发现这个地方做了“已经被删除”的标记,则跳过它,继续探测下面的。
那么这个地方就不能用了么?答案显然是可以用。咋用呢?很简单,当一个新的数据准备插入的时候,先按照查询那一套,尽可能的转一圈,看看自己之前是不是已经插入过了,如果没有的话,就把刚刚路上遇到的第一个带有“已经被删除”的地方插入新的数据,同时将“已经被删除”标记删除。

再哈希法

再哈希法其实就是准备多个哈希函数。将一个key输入到hashFunc1中,得到哈希值,发现里面有东西了,那就继续用第二个哈希函数…直到没有冲突产生。那么其查找方式也就很清楚了对吧,而删除操作同理不能直接删除,要做标记的。

rehash

rehash的意思是说,当哈希表的负载达到极限时,哈希表会成倍的扩充容量,然后将哈希表中的所有元素重新执行哈希过程,放入新的表中。一般会创建一张新的哈希表,其bucket桶的数量翻倍,为新的哈希表分配内存空间,然后逐个将旧的表中的元素再次计算一次哈希值并放入新的表中,删除旧的表,完成操作。
rehash的条件是啥呢?咱们讲redis下:
hashtable元素总个数 / 字典的链个数 = 每个链平均存储的元素个数(load_factor)
服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,load_factor >= 1,dict就会触发扩大操作rehash
服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,load_factor >= 5,dict就会触发扩大操作rehash
load_factor < 0.1,dict就会触发缩减操作rehash

哈希表中桶的个数为何是质数

桶的概念我刚刚没有介绍,其实就是链地址法中的链表,只不过链表没法停下来,如果哈希冲突严重的话会耗时很大,而桶的话就固定了冲突的最大容量。
桶是质数,其实在线性地址探测上,可以确保不会出现死循环,能遍历到所有的桶元素。(因为质数无法被除了1和自身以外的其他数整除,也就不会造成死循环)

代码实现

由于链地址法实在是太简单了,所以咱们实现一个开放地址法,用线性探测方法。
仔细想了一下,也真的好简单,直接用vector<pair<bool, pair<int, void*> > >即可,过了。当然你喜欢用tuple也很不错。

猜你喜欢

转载自blog.csdn.net/weixin_44039270/article/details/106595443
今日推荐