散列表设计思路,(可对照HashMap的源码)

https://blog.csdn.net/u010530712/article/details/93166430    这篇文章里初步介绍了散列表,及解决散列冲突的几种方案。目前来说,链表法是比较常用的方法,像我们熟悉的hashmap就是用的这个。

查找时通过hash(key)找到某个bucket,然后遍历链表,使得整体的查找时间复杂度为O(k),k是链表的节点数。

散列表碰撞攻击

hash(key)的算法是决定整个查询效率的重要因素。如果散列函数设计的不好,被有心人设计,将大量的key-value存到一个bucket的链表(如下图),然后查询就变成了遍历整条链表,使时间复杂度从O(1)变成了O(n)。举个例子,如果有10万条数据都存在这里,假设正常时1条数据的查询时间是0.1秒,当变成一条链时,时间变成了10000秒(2.78小时),可想而知,长时间的查询操作,肯定会消耗大量cpu资源,导致其他请求无法被响应,最终导致Dos攻击的目的。

因此在设计散列表时,除了考虑散列冲突,性能等因素外,如何能抵抗散列表碰撞攻击,也很重要

散列函数设计思路

1、原则:1)设计不能太复杂,复杂的计算,也会影响性能    2)生成的结果要尽量随机均匀分布,这样在源头上,减少甚至是避免散列冲突,即便是出现冲突,挂在一个bucket上的链表也不会太多。

2、如何应对装载因子过大

上篇介绍,装载因子=填入表中的个数/散列表的长度,因此装载因子越大,说明填入的数据越多,发生散列冲突的可能性也越高。但是我们无法预估要加入的数据个数,因此,我们可以通过借鉴数组的“动态扩容”来解决这个问题。

散列表的动态扩容根据装载因子的大小而定,我们可以自己定义触发动态扩容的装载因子大小,就像HashMap初始设定的是0.75。其次不同于数组,扩容后,原先存储的数据需要再通过散列函数计算存储位置,因此存储位置有可能会变化,如下图。

对于复杂度问题,插入一个数据,最好情况下,不需要扩容,最好时间复杂度是 O(1)。最坏情况下,散列表装载因子过高,启动扩容,我们需要重新申请内存空间,重新计算哈希位置,并且搬移数据,所以时间复杂度是 O(n)。用摊还分析法,均摊情况下,时间复杂度接近最好情况,就是 O(1)

当然,对于空间敏感的情况,我们也可以设计当装载因子小于某个下限值时,进行缩容

3、如何进行高效扩容

我们设想这样的情况,当散列表里存了2G的数据,这时候插入一个数据,触发了扩容机制,在扩容是就需要把这2G的数据重新计算散列值,并拷贝到新的散列表,这个过程是很耗时的。因此这种一次性完成“扩容-拷贝”的流程是不可行的。

我们可以这样,当插入一条数据触发扩容后,就申请新的散列表空间,但不全部拷贝。新数据插入到新的散列表,同时选一条原来的数据也拷贝到新的散列表,之后每次插入操作都选一条数据拷贝到新散列表。如下图

如果这时候需要查找操作,先到新表里查,要是查不到就到就表里查。

猜你喜欢

转载自blog.csdn.net/u010530712/article/details/93194138