16 如何打造一个好的散列表

好的散列表特性

1、支持快速查询、插入、删除操作
2、内存占用合理,不能浪费过多时间
3、性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况。

如何实现这样一个好的散列表呢

1、设计一个合适的散列函数
2、定义装载因子,并且设置动态扩容策略
3、选择合适的散列冲突解决方法。

如何设计散列函数

1、散列函数设计不能太复杂

过于复杂的散列函数,势必会消耗太多计算时间,也就间接影像散列表性能。
例如在实现word单词拼写检查中,这里的散列函数可以设置为:将单词中的每个字母的ascii编码进位相加,然后再跟散列表的大小求余、取模作为散列值,例如英文单词nice,转化出来的散列值就是这样的:

hash("nice")=(("n" - "a") * 26*26*26 + ("i" - "a")*26*26 + ("c" - "a")*26+ ("e"-"a")) / 78978

2、散列函数生成的值要尽可能的随机且均匀

这样才能避免或者最小化散列冲突,即使出现冲突,散列到每个槽位的数据也会平均,才不会出现某个槽内数据特别多的情况。

装载因子过大怎么办

装载因子越大,说明散列表元素越多,空闲位置越少,散列冲突发生的概率越大,不仅插入数据的过程要多次寻址或者拉很长的链,查的过程也会很慢。

对于静态数据,没有频繁的插入和删除操作,可以很容易根据数据的特点特性、分布从而设计出合适的散列函数。

但是对于动态数据,数据的频繁变动的,动态散列表中的装载因子也会逐渐变多,大到某种程度后散列冲突就变得不可接受。

这时候需要进行动态扩容,例如原来的装载因子是0.8,动态扩容后散列表空间变成原来两倍后,散列因子就变成了0.4,下降了一半。

但是散列表动态扩容在数据迁移操作会比数组数据迁移难,因为散列表的大小变了,导致通过hash计算的存储位置也变了,需要重新通过hash散列函数重新计算每个数据的存储位置。
例如:
在这里插入图片描述

对于插入数据:最好情况是不需要扩容,时间复杂度是O(1),最坏情况是装载因子过高,然后申请扩容,时间复杂度是O(n)。
对于删除数据:如果对空间敏感,可以在装载因子小于某个阈值时对散列表进行动态缩容。如果在意效率,能容忍多消耗一点内存空间,就不需要缩容。

高效扩容:
正常的扩容很耗时,可以通过这种方法实现高效扩容,将扩容操作穿插在插入操作的过程中完成:
1、当装载因子到达阈值后,只申请新的内存空间,但是不迁移数据。
2、当有新数据插入时,将新数据插入到新散列表中,同时将一条旧数据从旧散列表取出,存放到新散列表中。
3、通过多次以上插入数据的操作,所有的旧散列表的数据就全部一点一点迁移到了新散列表,这样效率就提高了很多。

如何解决散列冲突方法:

开放寻址法

优点:

1、散列表存储的数据都在数组中,cpu读取数据是将内存中数据一块一块读取到cpu缓存的,因为数据的连续性,所以读取的每一块都有我们想要的数据,下一次读数据的时候就直接从cpu缓存读取数据,降低了读内存的时间消耗。
2、因为数组的连续性,这样的散列表序列化起来比较简单。

缺点:

1、删除数据的时候需要特殊标记已经删除的数据,且所有数据存储在数组中,冲突的代价更高。所以导致开放寻址法的散列表上限不能太大,同时也比链表法更浪费空间。
总结:
当数据量小的时候、装载因子小的时候,适合用开放寻址法。例如java的ThreadLocalMap

链表法

优点:

1、对内存的利用率比开放寻址法高,不需要像开放寻址法那也提前申请。
2、对大装载因子的容忍度更高,寻址法只适用装载因子小于1的情况,但是链表法的装载因子是可以大于1的,即使装载因子变成10,但是只要散列函数的散列值均匀分布,也只是链表变长,虽然查找效率变低,但是比顺序查找还是快。

缺点:

1、执行效率比寻址法低。因为链表是零散分布的,对cpu缓存不友好,导致执行效率偏低。
2、因为要存储指针,所以对小的对象的存储比较耗费内存,当然如果是大对象,对象大小远远大于指针的大小,那么指针在内存的消耗就可以忽略了。
总结
基于链表的散列冲突解决方法适合用存储大对象、大数据量的散列表,且对比寻址法更灵活,支持更多的优化策略,比如用红黑树代替链表之类。

举例:HashMap

1、hashmap初始大小是16,当然这个默认值可以设置,如果预先直到数据量的大概数据量大小,可以更改初始大小,减少动态扩容次数,大大提高hashmap性能。
2、装载因子和动态扩容:最大装载因子是0.75,当hashmap的元素个数超过了0.75*capacity(capacity 表示散列表的容量),就会动态扩容。
3、散列方法:hashmap底层采用了链表法解决散列冲突,但是为了避免拉链过长,严重影响hashmap性能,jdk1.8的适合引用了红黑树,当链表长度超过了8时,链表就转为红黑树,利用红黑树快速增删改查的特点提高hashmap的性能,当链表长度小于6时,又降为链表,因为小数据量时,红黑树的优势不比链表大。
4、散列函数:hashmap的散列函数追求的是简单、均匀

int hash(Object key) {
    int h = key.hashCode()return (h ^ (h >>> 16)) & (capicity -1); //capicity表示散列表的大小
}

其中hashcode()返回的是java对象的hashcode(),比如string类型的hashcode()是:

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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值