停不下来使用的散列表

散列表(hash table)

       散列思想:散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来,如果没有数组,就没有散列表。
       散列表用的就是数组支持按照下标随机访问的时候,时间复杂度为O(1)的特性,通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置,当我们按照键值查询元素时,用同样的散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。
       散列函数伪代码

int hash(String key) {
  // 获取后两位字符
  string lastTwoChars = key.substr(length-2, length);
  // 将后两位字符转换为整数
  int hashValue = convert lastTwoChas to int-type;
  return hashValue;
}

       构造散列函数设计的基本要求
       1.散列函数计算得到的散列值是一个非负整数
       2.如果key1 = key2,那hash(key1) == hash(key2)
       3.如果 key1≠key2,那hash(key1)≠hash(key2) (这种基本无论什么算法,都无法避免散列冲突)

散列冲突

       1.开放地址法
       如果出现了散列冲突,就重新探测一个空闲位置,将其插入,重新探测新的位置采用的探测方法是线性探测。(当某个数据经过散列函数,存储位置已经占用,就从当前位置开始,一次往后寻找,看时候有空闲位置,直到找到为止)
       还有二次探测,所谓二次探测,跟线性探测很像,线性探测每次探测的步长是 1,那它探测的下标序列就是 hash(key)+0,hash(key)+1,hash(key)+2……而二次探测探测的步长就变成了原来的“二次方”,也就是说,它探测的下标序列就是 hash(key)+0,hash(key)+12,hash(key)+22……
       所谓双重散列,意思就是不仅要使用一个散列函数。我们使用一组散列函数 hash1(key),hash2(key),hash3(key)……我们先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置

       我们用装载因子来表示空位的多少
       装载因子计算公式:
       散列表的装载因子 = 填入表中的元素个数 / 散列表的长度

2.链表法(更加常用)

在这里插入图片描述

       当插入的时候,我们只需要通过散列函数计算出对应的散列槽位,其插入到对应链表中即可,所以插入的时间复杂度是 O(1)。

       如何设计散列函数
       散列函数的设计不能太复杂,其次,散列函数生成的值要尽可能随机并且随机分布,这样才能避免或者最小化散列冲突。
       装载因子过大怎么办,装载因子越大,说明散列表中的元素越多,空闲位置越少,散列冲突的概率越大,不仅插入数据的过程要多次寻址或者拉很长的链,查找的过程也会变得很慢。
       针对散列表,当装载因子过大时,可以进行动态扩容。当然,对空间消耗敏感,也可以在装载因子小于某个值的时候,启动动态缩容。
避免低效的扩容,可以将扩容操作穿插在插入操作的过程中,分批完成,对于查询操作,为了兼容新,老散列表中的数据,先从新散列表中查找,再去老的散列表中查找。

    两种使用场景

       使用开放地址法的是数据量比较小,装载因子小的时候,适合采用开放寻址法。这也是java中ThreadLocalMap使用开放寻址法解决散列冲突的原因
       基于链表的散列冲突处理方法比较适合存储大对象,大存储量的散列表,而且,比起开放寻址法,更加灵活,支持更多优化策略,比如用红黑树代替链表。

HashMap就是一个工业级别的散列表
       1.初始大小
       默认初始大小为16,这个默认值可以设置。
       2.装载因子和动态扩容
       最大装载因子默认0.75,当hashmap中元素个数超过0.75*capacity(容量)的时候,就会启动扩容,每次扩容为原来的两倍大小
       3.散列冲突的解决方法
       采用链表法来解决冲突,1.8之后,当链表长度太长(超过8),链表转为红黑树,当小于8个的时候,转为链表。
       4.散列函数
       int hash(Object key) {
               int h = key.hashCode();
               return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小
       }

LRU缓存淘汰算法

       一个缓存(cache)系统主要包含
       1.往缓存中添加一个数据
       2.从缓存中删除一个数据
       3.在缓存中查找一个数据
       每个操作都设计“查找操作”,单纯采用链表,时间复杂度只能是O(n),将散列表和链表两种数据结构组合使用,可以将时间复杂度将为O(1),

       使用双向链表存储数据。
       如何查找一个数据,通过散列表,在缓存中找到一个数据,找到数据之后,将他移动到双向链表的尾部
       如何删除一个数据,找到数据所在的节点,然后将结点删除。
       如何添加一个数据,先看这个数据是否在缓存中,如果在,移动到双向链表尾部,如果不在,就看缓存满没满,如果满了,则将双向链表头部的结点删掉,然后将数据放到链表尾部,如果没有,则直接放到尾部。

2.Redis有序集合

       2.1.什么是有序集合?
       ①在有序集合中,每个成员对象有2个重要的属性,即key(键值)和score(分值)。
       ②不仅会通过score来查找数据,还会通过key来查找数据。
       2.2.有序集合的操作有哪些?
       举个例子,比如用户积分排行榜有这样一个功能:可以通过用户ID来查找积分信息,也可以通过积分区间来查找用户ID。这里用户ID就是key,积分就是score。所以,有序集合的操作如下:
①添加一个对象;
②根据键值删除一个对象;
③根据键值查找一个成员对象;
④根据分值区间查找数据,比如查找积分在[100.356]之间的成员对象;
⑤按照分值从小到大排序成员变量。
       这时可以按照分值将成员对象组织成跳表结构,按照键值构建一个散列表。那么上面的所有操作都非常高效。

Java LinkedHashMap

       LinkedHashMap也是通过散列表和链表组合在一起实现的,实际上,不仅支持按照插入顺序遍历数据, 还支持按照访问顺序来遍历数据。
       LinkedHashMap是通过双向链表和散列表这两种数据结构组合实现的,LinkedHashMap中的LInked实际上就是指的双向链表,并非指用链表法解决散列冲突。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章