跳表
链表加多级索引的结构就是跳表
跳表中快速的插入、删除、查询任意数据的时间复杂度是O(logn);跳表的空间复杂度是O(n)
散列表
-
散列表英文叫hash table,就是哈希表或者hash表。
-
散列表用的事数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一中扩展,由数组演化而来。可以说,如果没有数组就没有散列表。
-
散列表中有键、散列函数、散列值
散列函数,就是一个函数,它可以定义成hash(key),其中可以表示元素的键值,hash(key)的值表示经过散列函数计算得到的散列值。散列函数设计的基本要求:
-
散列函数计算得到的散列值是一个非负整数;
-
如果key1=key2,那么hash(key1)==hash(key2);
-
如果key1 ≠ key2,那么hash(key1) ≠ hash(key2)
散列函数的第三点很难解决,即使经典的md5等也很难解决,一般采用两种方法:
1、开放寻址法
开放寻址冲突解决方法,除了线性探测方法之外,还有另外两种比较经典的探测方法,二次探测和双重散列
不管采用哪种探测方法,当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列的操作效率,我们会尽可能保证散列表中有一定比例的空闲槽位,用装载因子表示空位的多少。> 散列表的装载因子=填入表中的元素个数/散列表的长度 装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。
2、链表法
在散列表中,每个“桶(bucket)”或者“槽(slot)”会对应一条链表,所有散列值相同的元素我们都放到相同槽位对应的链表中
如何打造工业级水平的散列表
设计散列函数
散列函数设计不能太复杂,过于复杂的散列函数,会消耗很多计算时间,间接的影响到散列表的性能,其次,散列函数生成的值要尽可能随机并且均匀分布,这样才能避免最小化散列冲突,即便出现冲突,散列到每个槽里的数据也会比较平均不会出现某个槽内数据特别多的情况。
装载因子过大了怎么办?
对于静态的数据集合来说比较好操作,对于数据集合是频繁变动的,我们事先无法预估将要加入的数据个数,所以我们无法事先申请足够大的散列表,随着数据慢慢加入,装载因子就会慢慢变大,当达到一定程度后散列冲突就会变得不可接受。这时候想到的就是动态扩容。对于散列表的扩容,数据搬移操作就会复杂很多,因为散列表的大小变了,数据搬移操作要复杂很多,因为散列表的大小便了,数据位置变了,所以需要通过散列函数重新计算每个数据的存储位置。
装载因子阈值需要选择得当。如果太大,会导致冲突过多;如果太小,会导致内存浪费严重。装载因子阈值的设置要权衡时间、空间复杂度。如果内存空间不紧张,对执行效率要求很高,可以降低负载因子的阈值;相反,如果内存空间紧张,对执行效率要求又不高,可以增加负载因子的值,甚至可以大于 1。
如何避免低效的扩容
为了解决一次性扩容耗时过多的情况,我们可以将扩容操作穿插在插入操作的过程中,分批完成。
当有新数据要插入时,我们将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,我们都重复上面的过程。经过多次插入操作之后,老的散列表中的数据就一点一点全部搬移到新散列表中了。
通过这样均摊的方法,将一次性扩容的代价,均摊到多次插入操作中,就避免了一次性扩容耗时过多的情况。这种实现方式,任何情况下,插入一个数据的时间复杂度都是 O(1)。
如何选择冲突解决方法
1、开放寻址法:散列表中的数据都存储在数组中,可以有效地利用cpu缓存加速查询速度。
当数据量比较小、装载因子小的时候,适合采用开放寻址法。这也是java中的threadlocalMap使用开放寻址法解决散列冲突的原因。
2、链表法:基于链表的散列冲突处理方法比较适合存储大对象,大数据量的散列表,而且比起开放寻址法,它更加灵活,支持更多的优化策略,比如红黑树代替链表。
工业级散列表举例(hashmap)
1、初始大小:hashmap默认初始大小是16;
2、转载因子和动态扩容:最大装载因子默认是0.75,当hashmap中元素个数超过0.75*capacity的时候,就会启动扩容,每次扩容都会扩容成原来的两倍。
3、散列冲突的解决办法:hashmap底层采用了链表法来解决冲突,一旦出现拉链过长,就会严重影响hashmap的性能。jdk1.8中引入了红黑树,当链表长度大于8时,就转换为红黑树,当红黑树结点个数少于8时,将红黑树转换成链表。
4、散列函数
int hash(Object key) {
int h = key.hashCode();
return (h ^ (h >>> 16)) & (capicity -1); //capicity表示散列表的大小
}
散列表和链表经常一起使用
散列表这种数据结构虽然支持非常高效的数据插入、删除、查找操作,但是散列表中的数据都是通过散列函数打乱之后无规律存储的。也就说,它无法支持按照某种顺序快速地遍历数据。如果希望按照顺序遍历散列表中的数据,那我们需要将散列表中的数据拷贝到数组中,然后排序,再遍历。
因为散列表是动态数据结构,不停地有数据的插入、删除,所以每当我们希望按顺序遍历散列表中的数据的时候,都需要先排序,那效率势必会很低。为了解决这个问题,我们将散列表和链表(或者跳表)结合在一起使用。
linkedHashmap 是通过双向链表和散列表这两种数据结构组合实现的,linkedhashmap中的linked实际上指的是双向链表,并非指用连败哦发解决散列冲突。
哈希算法
什么是哈希算法
将任意长度的二进制值串映射为固定长度的二进制值串,这个映射的规则就是哈希算法,而通过原始数据映射之后得到的二进制值串就是哈希值。
优秀的哈希算法需要满足几点要求:
- 从哈希值不能反向推导出原始数据(哈希算法也叫单向哈希算法)
- 对输入数据非常敏感哪怕原始数据只修改了一个bit,最后得到的哈希值也大不相同;
- 散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小;
- 哈希算法的执行效率要尽量高效,针对较长文本,也能快速地计算出哈希值。
哈希算法应用
- 安全加密 md5、sha、des、aes;
- 唯一标识
- 数据校验
- 散列函数
- 负载均衡:通过哈希算法,对客户端ip地址计算哈希值,将取得的哈希值与服务器列表的大小进行取模运算,最终得到的值就是应该被路由到服务器编号。
- 数据分片:统计搜索关键词出现的次数?可以用n台机器并行处理,从搜索记录的日志文件中,依次独处每个搜索关键词并且通过哈希函数计算哈希值,然后在根n取模,最终得到的值就是应该被分配到机器的编号。
- 分布式存储:面对互联网上的海量的数据和用户我们为了提高读写能力会选择分布式存储,利用数据分片的思想,通过哈希算法对数据取哈希值,然后对机器个数取模,最终存储到响应的机器中去。但是如果机器不够用了,需要扩容,这时候对于原来存储的数据就不一定存储在原来的机器上了需要重新分配,这就遇到了雪崩效应,为此,一致性哈希算法就要登场了~~~一致性哈希