大家熟悉的哈希qwq
哈希
哈希在我的理解里就是,一一对应!
比如我们把某个字符串A,对应为某个数字x鸭,某个字符串B,对应为某个数字y鸭;把某个图片对应为某个数字鸭,之类der……
根据这个特性, 理想的哈希应该能满足一些很重要的特性:
- 对应是不变的,如A不管什么时间什么地点,永远对应的是x;
- 对应是不冲突的,如只有A可以对应x,其他字符串不能对应到x;
第一个特性很好得到,只要我们的哈希函数确定,显然无论什么时候,只要哈希函数不变,就会一直对应。
但是第二个特性就不那么好做到了,只能存在于理想中了……
哈希冲突
我们把不同的字符串,对应到了同一个位置/数字,称之为哈希冲突。
对应哈希冲突,有一些常见的解决方法。
(我个人认为,发生哈希冲突时,首先要着眼的是哈希函数的设计,如果哈希函数设计不当,哈希甚至会退化成线性表(画面太美,不敢看))
教科书中的解决方案
我们首先把问题简化一下,看做是将某个数据哈希到array[hashcode]
中,换句话说,就是拿数组下标当哈希值,里面存放对应的内容。
线性探测法
对于哈希冲突,比如A和B,我们将array[hash(A)] = A
。此时放入B,但是hash(A) == hash(B)
。
这种情况下,我们不断的去寻找,空着的位置在哪里……
因为这里array[hash(B)(也就是hash(A))
已经有人住了,所以我们就在这个数组下标中一个一个往后寻找,直到找到一个空位,安插进去,就是B所在的位置啦。
这样做很多缺点:
- 有集聚效应,当发生冲突后,因为占用了别人的位置(那个位置可能是
hash(C)
的位置阿喂!),所以之后可能会很频繁的发生冲突。
- 难以删除,因为很可能一串都是同一个哈希数的家,所以中间删除一个以后,后面的可能会难以查找了。因此只能对删除的元素打标记,但是这样又可能导致空间浪费……
为了解决这些问题,也有一些方案。比如发生冲突时,我们不再是一个一个向后寻找,而是隔几个向后寻找……或是对不同的hash值,跳跃的个数不同……以此来避免上述的一些问题。
再哈希法
顾名思义,发生冲突后,对该元素使用另一个哈希函数,再一次哈希以确定应该放置的位置。
拉链法
对于每一个数组位置,放置的元素相当于一个链表。
当有冲突时,我们将这个元素插入到链表尾部,以此来避免冲突。
哈希中的装载因子
哈希中,我们的数组有多少个,相当于我们有多少个bucket,装载因子是:总键值对数/箱子个数 ,它体现了哈希表的【满】的程度。
该数值越大,意味着哈希表越满,越容易导致冲突,性能也就越低。
大多哈希的实现,当装载因子达到0.75以上后,一般就会自动扩容。
哈希扩容中,一般会创建两倍大小的bucket,再将之前所有的数据,进行再次哈希,装载到新箱子中。
这里容易成为性能瓶颈,一方面,可能某个用户一来,恰好进行了扩容操作,那么对于该用户来说,是不公平的(为什么我等待时间这么长5555);另一方面,所有哈希值可能都没用,也太弟弟了!
在这里有一个结论: 扩容扩两倍,原有的哈希值发生变动的可能和情况最少!
一致性哈希
为了避免可能的再次哈希情况,引入了一致性哈希。
一致性哈希也经常用于分布式实现中。
我们把哈希值唯一确定,之后映射到一个圆环中。
那么,当分布式出现崩溃或新增节点的情况时,只会影响圆环中的一部分。
这样可能会出现没有考虑到不同节点的性能不同的问题,或者负载不够均衡的问题。
我们可以进一步引入虚拟节点,再把多个虚拟节点向实际节点映射,完事~。
(感觉计算机很多方面的问题,都可以通过引入抽象层解决)