数据结构与算法 --散列表(十一)

一、散列思想


 1、散列表用的是数组支持下标随机访问的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,没有数组就没有散列表

 2、得到数组下标的映射方法就叫做散列函数(hash函数),而散列函数计算得到的值就是散列值(hash值)

规律:散列表用的是数组支持下标随机访问的时候,时间复杂度为是O(1)的特性。通过散列函数将元素的键值映射为下标,将数据存在数组中对应下标的位置。当为什么按照键值查询元素时,我们用同样的散列函数,将键值转化为数组下标,从对应的数组下标的位置取数据

二、散列函数


散列函数设计的基本要求:

  1、散列函数计算得到的散列值时一个非负整数

  2、如果key1=key2,那么hash(key1)=hash(key2)

  3、如果key1≠key2,那么hash(key1)≠hash(key2) 真实情况下,不同的key对应的散列值都不一样,几乎不可能。如MD5.SHA.CRC等哈希算法

散列冲突

解决冲突的方法:开放寻址法和链表法

1、开放寻址法

核心思想:如果出现了冲突,就重新探测一个空闲位置,将其插入。如何从新探测呢?

  •    线性探测,当我们插入数据时发现经过散列函数后,存储的位置被占用,就从当前位置开始,依次向后查找,看是否有空闲位置,直到找到为止
  •    二次探测   hash(key) +0,hash(key)+1^2,hash(key)+2^2
  •    双重散列   第一个散列函数位置被占用就再用一个散列函数,依次类推,直到找到空闲的存储位置

不管采用哪种探测方法,当散列表空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,尽可能保证散列表有一定比例的空闲槽。用装载因子来表示空位的多少

装载因子的计算公式是:

 散列表的装载因子 = 填入表中的元素个数 / 散列表的长度

装载因子越大,出现 冲突的几率越大,散列表性能下降

2 、链表法

 插入时,通过散列函数得到对应的散列槽位,将其 插入到对应链表中即可。时间复杂度是O(1)

查找、删除一个元素时,同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除。此时间复杂度和链表的长度k成正比,也就是O(K)。对于散列均匀的散列函数来说,理论上  k = n / m,其中 n 表示散列中的数据个数,m表示散列表中 “槽” 的个数

问题:

1、Word文档单词拼写查找功能时如何实现的?

 常用单词20w个左右,假设单词平均长度10字母,平均一个单词占用10字节的内存空间,那20w大约占2MB的存储空间,就算放大10倍时20MB.可以完全放在内存里面。所以我们可以用散列表存储整个英文单词词典,当输入一个单词时候,我们就去散列表中去查找。如果查到,说明拼写正确。反之,不正确

2、假设我们有 10 万条 URL 访问日志,如何,按访问次数给URL排序?

  遍历10万条数据,以URL为key,访问次数为value, 存入散列表。同时记录访问次数的最大值,时间复杂度为O(n)

  如果 K 不是很大,可以使用桶排序,时间复杂度 O(N)。如果 K 非常大(比如大于 10 万),就使用快速排序,复杂度 O(NlogN)

3、有两个字符串数组,每个数组大约有 10 万条字符串,如何快速找出两个数组中相同的字符串?
以第一个字符串数组构建散列表,key 为字符串,value 为出现次数。再遍历第二个字符串数组,以字符串为 key 在散列表中查找,如果 value 大于零,说明存在相同字符串。时间复杂度 O(N)。

 

三、如何设计散列函数?


1、散列函数不能态复杂,太复杂的函数,势必耗费更多的计算时间,间接影响性能

2、散列函数生成的散列值尽可能的随机,减少 冲突。即使出现冲突,散列到每个槽的数据也会平均,不会出现某个槽内数据特别多的情况

常见的散列函数:

  • 直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。
  • 数据分析法:提取关键字中取值比较均匀的数字作为哈希地址。
  • 除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。
  • 伪随机数法:采用一个伪随机数当作哈希函数。

装载因子过大怎么办?

 动态扩容

 插入一个数,不需要扩容,最好的时间复杂度为O(1)。最坏情况下,需要扩容,需要重新申请内存空间,重新计算hash位置,并且搬移数据,时间复杂度为O(n)。用均摊分析法,均摊的情况下时间复杂度接近最好情况,为O(1)

如何避免低效的扩容?

 大部分情况下,插入一个数据都很快,但是当装载因子达到阈值时,需要进行扩容,这时候插入一个数据时会变的很慢,当数据足够多的时候就难以接受

为了解决一次性扩容耗时过多的情况,我们可以将扩容操作穿插在插入操作的过程中,分批完成。当装载因子触达阈值之后,我们只申请新空间,但并不将老的数据搬移到新散列表中。将一次性扩宽的代价均摊到每次插入的操作 中,这种情况下的时间复杂度为O(1)

如何选择冲突解决办法?

 1、开放寻址法:

 优点:借助CPU缓存机制(局部性原理)加快查找速度,并且序列化简单

 缺点:删除数据的时候麻烦,需要特殊标记已经删除的数据,所有数据都存在一个数组,冲突的代价更高

 总结:数据量比较小,装载因子小的时候,适合采用开放寻址法。这也是Java中的ThreadLocalMap使用的开放寻址法解决冲突的原因

2、链表法:

 总结一下,基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。

工业级散列表例子

JAVA中使用散列表的数据类型:
HashTable:
1、默认初始大小:11
2、装载因子:0.75
3、散列函数:int hash = key.hashCode();
                      int index = (hash & 0x7FFFFFFF) % tab.length;
4、当装载因子大于0.75时,启动扩容机制
4、冲突解决方法:使用单链表解决hash冲突
HashMap:
1、默认初始大小:16
2、装载因子:0.75
3、散列函数:
        hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
4、当装载因子大于0.75时,启动扩容机制
5、使用单链表解决hash冲突,当链表长度大于8,将单链表转换成红黑树
ThreadLocalMap
1、初始容量:16
2、装载因子:2/3
3、散列函数:
    hash(Object key) {
        int HASH_INCREMENT = 0x61c88647;
        AtomicInteger nextHashCode = new AtomicInteger();
        nextHashCode.getAndAdd(HASH_INCREMENT)
        int threadLocalHashCode = nextHashCode()
        int i = threadLocalHashCode & (table.length - 1);
    }
4、当装载因子大于2/3时,启动扩容机制
5、使用线性探测的开放地址法解决hash冲突

问题:

如何设计一个工业级的散列函数?

首先思考,何为一个工业级达到散列函数,具有哪些特性?

1、支持快速查找,插入,删除操作

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

3、性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况

如何实现一个散列表?

1、选择合适的散列函数

2、定义适中的装载因子阈值,并设计动态扩容策略

3、选择合适的散列 冲突解决方法

HashMap的loadFactor为什么是0.75? 

https://www.jianshu.com/p/64f6de3ffcc1

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值