散列表学习总结

内容来自对极客时间数据结构与算法之美课程的总结;极客时间版权所有: https://time.geekbang.org/column/article/4ca189be982362cbe15145a1b586dd1b/share?code=iZfb%2FSZXA8tkVYLEdUTbeXy%2FHVCn4k4p8hkIJjxrcpI%3D&oss_token=7932ffb906392d3c

散列思想

散列表利用数组支持下标随机访问的特性,作为数组plus。.

装载因子

散列表的装载因子 = 填入表中的元素个数 / 散列表的长度
散列碰撞攻击原理:恶意攻击者输入恶意制作的数据,使得所有数据经过散列函数后都到一个桶里,如果解决冲突的办法是链表法,此时散列表就会退化成链表,查询时间复杂度急剧下降。最后有可能导致因为查询而消耗大量CPU以或线程资源,导致系统无法响应其他请求,从而达到了拒绝服务攻击的目的(DOS);
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200322165332214.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MDUyMjkwOQ==,size_16,color_FFFFFF,t_70

当散列表的装载因子超过某个阈值时,要进行扩容。装载因子的阈值设置要权衡时间、空间复杂度。如果内存空间不紧张,对执行效率要求很高,可以降低负载阈值;相反,如果内存空间紧张,对执行效率要求不高,可以增加负载因子的值,甚至可以大于一。

扩容

避免低效地扩容当装载因子已经达到阈值时,需要先进行扩容,再插入数据。扩容分为两步,一是申请空间,二是搬移数据。一次性扩容耗时过多,我们可以将搬移数据穿插在插入过程中,分批完成。当新数据插入时,我们将新数据插入到新的散列表中,并且从老的散列表中取一个数据放入到新的散列表。重复多次后旧散列表中的数据就一点点全部搬到新散列表中了。查找时,先在的散列表中查找,再在的散列表中查找。
java中LinkedHashMap采用了链表解决冲突,ThreadLocalMap是通过线性探测的开放寻址法来解决冲突。

**

开放寻址法*

*线性探测和平方探测等等
*

优点

*数据存储在数组中,可以有效地利用CPU缓存加快查询速度
序列化比较容易;

缺点

删除数据时麻烦,需要做标记;所有数据都存在一个数组,冲突代价更高因此,装载银子不能过大,所以比较浪费内存空间

总结
数据量较小、装载因子小时适合采用开放寻址法。

链表法

优点

内存利用率高 。因为需要就创建,开放寻址需要提前申请好。(也就是链表的优点)
对大装载因子的容忍度跟高。开放寻址法只适用于装载因子小于1的情况。接近1时都可能会有大量的散列冲突,导致大量的探测、再散列等。而链表法,只要散列函数的值随机均匀,即便是装载因子编程10,也只是链表变长,查找效率即使下降,也比顺序查找快。

缺点

事实上指针的存储时比较消耗内存的。再者,链表中的结点零散分布在内存中,***不连续,对CPU缓存***不友好,也会影响执行效率。
不过如果是大对象,对象远大于指针(4个字节或者8个字节的大小),那链表中指针的内存消耗就可以忽略啦。

优化
链表法中的链表可以替换为其他的高效的数据结构,比如跳表红黑树。这样即便所有数据都散列到一个坐标下,最终退化的散列表的查找时间也不过是O(logn)。这个优化可以抵御散列碰撞攻击

总结
链表法适合存储大对象、大数据量的散列表,比起开放寻址法,它更加灵活,支持更多的优化策略。

工业级散列表举例

HashMap

初始大小
默认是16,可以修改初始值大小
装载因子和动态扩容
阈值默认0.75,超过0.75*capacity时,启动自动扩容,每次扩容两倍。
散列解决冲突的办法
底层时链表法,jdk1.8之后引入了红黑树,当链表长度>8时,链表转换为红黑树。还可利用红黑树快速增删改查的特点,提高HashMap的性能。当红黑树结点个数小于8时,红黑树又会转化为链表。CAUSE红黑树在节点数量较小时维护平衡,比起链表,性能优势并不明显。
** 散列函数**

int hash(Object key) {
     
    int h = key.hashCode()return (h ^ (h >>> 16)) & (capicity -1); //capicity 表示散列表的大小
}
public int hashCode() {
     
  int var1 = this.hash;
  if(var1 == 0 && this.value.length > 0) {
     
    char[] var2 = this.value;
    for(int var3 = 0; var3 < this.value.length; ++var3) {
     
      var1 = 31 * var1 + var2[var3];
    }
    this.hash = var1;
  }
  return var1;
}

总结

工业级散列表特性:

  • 支持快速查询,插入,删除操作

  • 内存占用合理,不能浪费过多的内存空间

  • 性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况
    如何实现

  • 设计一个合适的散列函数

  • 设置一个合理的装载因子阈值,并设计动态扩容策略 (随着数据的不断增加动态扩容是不可避免地)

  • 选择合适的散列冲突的解决办法

散列表与链表好兄弟

  • hnext?
  • 3,插入后为啥么把原来的删除了?

散列表的应用

负载均衡

实现一个绘画粘滞的负载均衡(session sticky)的负载均衡算法呢?需要再同一个客户端,在一次会话中的所有请求都路由到同一服务器上。
通常是维护一张映射关系表,客户端IP或者会话ID与服务器编号的映射关系。

缺点

  • 客户端很多时,映射表可能会很大,比较浪费内存空间。
  • 客户端下线、上线、服务器扩容、缩容都会导致映射失效,这样维护映射表的成本就会很大

解决办法
借助哈希算法,对客户端IP地址或者绘画ID计算哈希值,将取得的哈希值与服务器列表的大小进行取模运算,最终得到的值就是应该被路由到的服务器编号。这样,同一个ip,就会对应同一个服务器编号。

数据分片

散列表学习总结_第1张图片

  1. 对于支持动态扩容的散列表,插入操作的时间复杂度是多少呢?
  2. url 假设有10万条URL访问日志,如何按照访问次数给URL排序?
  3. 有两个字符串数组,每个数组大约有10万条字符串,如何快速找出两个数组中相同的字符串?
  4. MapReduce
  5. 熟悉的编程语言中,哪些数据类型底层是基于散列表实现的?散列函数是如何设计的?散列冲突是通过哪种方法解决的?是否支持动态扩容呢?

2一第一个字符串数组构建散列表,key为字符串,value为出现次数。再遍历第二个字符串数组,以字符串为key在散列表中查找,如果value大于0,说明存在相同的字符串,时间复杂度O(N).

你可能感兴趣的:(数据结构)