4.11散列表

散列表上

1.抛出一个思考题开课

word中单词拼写检查功能如何实现的?

学完今天的内容你就微软office工程师一样,轻松实现这个功能

2.散列表

散列表用的就是数组支持按照下标[随机访问]数据的特性,实现时间复杂度o(1),所以散列表其实就是数组的一种扩展。

3.散列冲突

我们常用的散列冲突解决方法有两类,开放寻址法(open addressing)和链表法(chaining)。

i.开放寻址法

线性探测(+1)

二次探测(+1^2)

双重散列(再次散列)

用装载因子来观测空位多少,装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。

ii.链表法

在散列表中,每个“桶(bucket)”或者“槽(slot)”会对应一条链表,所有散列值相同的元素都放到相同槽位对应的链表中。

3.开篇解答

散列表存储单词字典,然后散列查询,查不到可能拼写错误

常用的英文单词有 20 万个左右,假设单词的平均长度是 10 个字母,平均一个单词占用 10 个字节的内存空间,那 20 万英文单词大约占 2MB 的存储空间,就算放大 10 倍也就是 20MB。对于现在的计算机来说,这个大小完全可以放在内存里面。所以我们可以用散列表来存储整个英文单词词典。

4.小结

散列表两个核心问题是散列函数设计和散列冲突解决。散列冲突有两种常用的解决方法,开放寻址法和链表法。

5.课后思考题

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

解:url为key,访问次数为value,10万*100B=20MB

有两个字符串数组,每个数组大约有 10 万条字符串,如何快速找出两个数组中相同的字符串?

解:一个字符串做参考串做key,出现次数为value,第二个字符串做检验串,遍历字符串查key,若value不为0则存在相同

散列表中

1.散列表碰撞攻击

在极端情况下,恶意攻击者,通过精心构造的数据,使得所有的数都散列到同一个槽。如使用的是基于链表的冲突解决方法,散列表就会退化为链表,查询的时间复杂度就从 O(1) 急剧退化为 O(n)。这样就有可能因为查询操作消耗大量 CPU 或者线程资源,导致系统无法响应其他请求,从而达到拒绝服务攻击(DoS)的目的。这就是散列表碰撞攻击的基本原理(调虎离山)

2.如何设计工业级散列函数

工业级散列函数标准:

i.支持快速的查询、插入、删除操作;

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

iii.性能稳定,极端情况下,性能不会退到无法接受

如何实现这样一个散列表:

i.设计一个合适的散列函数;

ii.定义装载因子阈值,并且设计动态扩容策略;

iii.选择合适的散列冲突解决方法。

3.简单的随机函数

i.数据分析法

选择越随机的做key,可能重复的做value如下:

运动员编号做key其他信息做value

手机后四位做key前面各位做value

ii.进位相加

比如单词拼写检查,每位26个,

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

4.装载因子过大

静态数据集,容易设计散列函数,因为数据都是已知的

动态数据集,数据集合频繁变动,无法预估,有扩大和缩小两种

假设变大,数据集慢慢变大,装载因子也会变大,直到崩溃,借助前面所学的动态扩容,可以解决

i.数组动态扩容

假设原来装载因子0.8,现在扩容为原来2倍,装载因子变为0.4,然后数据搬移

ii.避免低效扩容

将新数据插入新散列表,并且从老的散列表中拿出一个数据放入到新散列表,一点点分批完成插入,查询就改为先查老表再查新表

假设变小,数据集慢慢变小,装载因子变小,如果内存吃紧,可以设置阀值动态缩容

5.冲突解决办法

数据量小、装载因子小,适合采用开放寻址法。这也是 Java 中的ThreadLocalMap使用开放寻址法解决散列冲突的原因。

大对象,大数据量,适合基于链表的散列冲突处理方法,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表,比如,Java 中 LinkedHashMap 就采用了链表法解决冲突。

6.工业级散列表:java中HashMap

i.初试大小

HashMap 默认的初始大小是 16,如果事先知道大概数据量多大,可修改默认初始大小,减少动态扩容的次数,这会提高HashMap 的性能。

ii.装载因子和动态扩容

最大装载因子默认是 0.75,当 HashMap 中元素个数超过 0.75*capacity(capacity 表示散列表的容量)的时候,就会启动扩容,每次扩容都会扩容为原来的两倍大小。

iii.散列冲突解决办法

HashMap 底层采用链表法来解决冲突,免不了会出现拉链过长的情况,会严重影响 HashMap 的性能。在 JDK1.8 版本,引入了红黑树。而当链表长度太长(默认超过 8)时,链表就转换为红黑树。可以利用红黑树快速增删改查的特点,提高 HashMap 的性能。当红黑树结点个数少于 8 个的时候,又会将红黑树转化为链表。因为在数据量较小的情况下,红黑树要维护平衡,比起链表来,性能上的优势并不明显。

iiii.散列函数设计

int hash(Object key) {

    int h = key.hashCode();

    return (h ^ (h >>> 16)) & (capitity -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;

}

散列表下

1.为何散列表和链表经常一起使用

链表实现插入删除o(1),散列表实现查询o(1)

比如LRU,跳表,LinkedHashMap

2.LRU缓存淘汰算法

在链表那一节中,借助散列表,可以把 LRU 缓存淘汰算法的时间复杂度降低为 O(1)。

思想:first in last out,一个链表,查询找到就放到末尾,查询不到插入也放到末尾(表示是最新加入的元素放最后删除),如果满存了,就删除头部元素

分析:三个操作:插入,删除,查询,最关键是查询o(n)如何让查询变为o(1),建立新的散列表(索引)来专门实现查询,原链表实现插入删除

实现:用双向链表存储数据,链表中7的每个结点处理存储数据(data)、前驱指针(prev)、后继指针(next)之外,还新增了一个特殊的字段 hnext,用来将结点串入到索引表链表中

注:这时每次结点都在两条链表中,一是双向链表中,二是索引表链表中

3.Redis有序集合

在有序集合中,每个成员对象有两个重要的属性,key(键值)和score(分值)。我们不仅会通过 score 来查找数据,还会通过 key 来查找数据。

i.添加一个成员对象;

ii.按照键值来删除一个成员对象;

iii.按照键值来查找一个成员对象;

iiii.按照分值区间查找数据,比如查找积分在 [100, 356] 之间的成员对象;

iiiii.按照分值从小到大排序成员变量;

解:i.ii.iii可以借助双向链表和索引表之链表法两个链表实现,iiii.iiiii可以用跳表实现,若要一起实现五个功能,肯定是两者合一,组成一个新的数据结构实现

4.Java中LinkedHashMap

LinkedHashMap 也是通过散列表和链表组合在一起实现的。实际上,它不仅支持按照插入顺序遍历数据,还支持按照访问顺序来遍历数据。

如:put(3,2)(1,10)(5,7)(2,9)

解:遍历结果是:3-1-5-7

说明是按照插入顺序遍历的的

如:put(3,2)(1,10)(5,7)(2,9)(3,25)get(5)

解:遍历结果是1-2-3-5

说明LinkedHashMap效果跟LRU一模一样,说明两者原理同,双向链表和散列表两表一起实现

注:LinkedHashMap 本身就是一个支持 LRU 缓存淘汰策略的缓存系统,Linked表示双向链表,HashMap表示链表法解决冲突,两者合并使用

5.课后思考

题一,为何使用双向链表不是单向链表?

因为单链表插入删除需分别定位到后一个位置,前一个位置,在操作,需要o(n),故使用双向链表

题二,10个对象,内容包括id和积分,需要实现

i.根据id快速查找,删除,插入积分信息

ii.积分在某个区间的所有id

iii.积分从小到大排序,查找任意区间[x,y]所有id

你可能感兴趣的:(4.11散列表)