散列表

作者: 范柏柏 | 来源:发表于2020-05-09 18:13 被阅读0次

定义

散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展。可以说,没有数组,就没有散列表。

通过散列函数把元素的键值映射为数组下标,然后将数据存储在数组中对应下标的位置。
当我们按照键值查询元素时,使用同样的散列函数,将键值转换为数组下标,从对应的数组下标位置取数据。

散列表原理.png

散列表有两个非常关键的知识点

  • 散列函数
  • 散列冲突

散列函数

顾名思义,它是一个函数。hash(key),入参是元素的key,返回为散列值也就是数组的下标。
所以就得到了散列函数的三个基本要求:

  • 散列函数计算得到的散列值是一个非负整数
  • 如果key1 == key2,那hash(key1) == hash(key2)
  • 如果key1 != key2,那hash(key1) != hash(key2)
    前两个很好实现,第三个业界暂时没有一个完美的无冲突的散列函数,所以要通过其他途径来解决。这个途径就是解决散列冲突。

散列冲突

当不同的key通过散列函数,得到了同一个hash值,也就是说对应了数组的同一个下标,数组一个节点不可能存两个元素,所以要解决散列冲突。
通常有两种解决hash冲突的办法,开放寻址法和链表法。

  • 开放寻址法就是:这个下标用不了了,就放他后面,他后面也有了,再往后找,直到找到。
  • 链表法: 这个index里存不了一个数,那就存一个链表呗。

散列函数和hash冲突的具体用法,在后文会聊hashmap,linkedHashMap等应用的时候具体展开。

java hashMap

1、初始大小

HashMap默认的初始大小是16,当然这个默认值是可以设置的,可以通过修改默认大小,减少动态扩容的次数。

2、解决冲突解决方法

HashMap底层采用链表法来解决冲突。但链表过长,会退化成单链表,严重影响性能。JDK8版本中,为了对HashMap做进一步优化,引入了红黑树。当链表长度超过8时,链表就会转换成红黑树。利用红黑树快速增删改查的特性,提高HashMap的性能。当红黑树节点数量小于8个的时候,又会将红黑树转化为链表。

3、散列函数

hashMap的散列函数直接上代码,看似非常简单,实则非常精妙(涉及到位运算的通常都非常长精妙)

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

先看h ^ (h >>> 16) 啥意思。因为int是32位的,让高16位 异或 低16位,增加高低位的参与度。
再看hash & (capicity - 1)。这段代码等价于 hash % capicity。为什么等价呢,因为capicity是2的整数倍(因为扩容是两倍两倍扩的, 你初始化大小为9, 内部也是开的大小也是16),二进制与运算相当于取模。

4、装载因子和动态扩容

装载因子默认是0.75,当HashMap中元素个数超过 0.75 * 数组size 的时候,就会触发扩容,每次扩容都会扩容为原来的两倍大小。

扩容,扩的是数组的长度,那么数组上挂的一串一串的链表呢?
答案是:一个一个rehash,重新找自己在哪个数组下标下,然后重新生成链表(红黑树)。扩容非常耗时,但通过散列函数,rehash也是用一样的散列函数,因为扩容是2的整数倍,所以扩容后计算出的hash值,要么是在原位置,要么是在原位置再移动2次幂的位置,降低了计算的复杂度。

扩容有一个坑啊
这也是什么hashMap线程不安全的原因。有两种情况。
1、在put的时候。两个线程A和B,A hash完落到了数组1中,找到了数组1中的头结点。时间片用完。B hash完也落在数组1中,找到数组1中的头结点,并在后面插入了B的key-value。注意:这个时候,A和B获取的头结点是一样的。B操作完,A继续操作,使用同一个头结点,在尾巴上插入A的key-value。这个时候,B的值被覆盖了。
怎么避免呢??在put的时候加锁就好了。
hashTable的做法是所有方法都加锁,使用synchronized,但并发太差,get也会加锁。
currentHashMap的做法是,将分成很多个map,每个map单独加锁,虽然在一个map中会有阻塞的情况,但其他的map不会阻塞。

2、1.7 在rehash的时候,链表可能会成环。1.8这个问题就没有了。

java LinkedHashMap

HashMap 底层是通过散列表这种数据结构实现的。而 LinkedHashMap 前面比 HashMap 多了一个“Linked”,这里的“Linked”是不是说,LinkedHashMap 是一个通过链表法解决散列冲突的散列表呢?实际上并没有这么简单。
先看下LinkedHashMap的特性:

LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

先上个图


LinkedHashMap.png

从图中可以看到,linkedHashMap实际上时使用的散列表 + 双向链表实现的。

散列表:作用还是为了O(1)查找。从而实现O(1)插入和删除。
双向链表:可以看到不仅仅是双向链表,是"三向链表"
next指针: 和hashMap一致,是挂在数组上的链表,hash冲突时,往这个链表后面挂

before和after指针才是linkedHashMap的双端指针。目的是为了排序。hashMap无序,那就根据插入顺序或者LRU循序,用链表给无序的节点串起来不就有序了嘛。

这样get/put/remove根据散列表O(1)查询到节点。
遍历,根据头指针,遍历双向链表。

1、初始大小

同hashMap

2、 解决冲突办法

还是链表法。linkedHashMap就不升级成红黑树了。

3、散列函数

同hashMap

4、装载因子和动态扩容

装载因子同hashMap
扩容唯一不同的地方是,hashMap是遍历散列表,数组中一个下标一个下标遍历。
linkedHashMap是从头指针遍历双向链表。为啥呢,当然还是因为有序啊。

相关文章

  • 散列表

    1.啥是散列表及散列函数? 很多语言都提供了散列表的实现方式,python是用dict{ }来实现 2.有啥优势?...

  • 散列表

    基本概念(非严谨) 散列表:按照思考事物本质以及理想状态的思路,那么散列表从本质来讲就是一个表,而理想的散列表应该...

  • 散列表

    散列表:散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f...

  • 散列表

    转载请注明出处!https://www.jianshu.com/p/e325578eb512 链表实现 Githu...

  • 散列表

    一、定义 散列表(Hash Table,也叫哈希表),是通过把键值映射成整数来作为数组的索引,并进行访问记录的一种...

  • 散列表

    https://blog.csdn.net/pcwl1206/article/details/83582986

  • 散列表

    散列查找法的两项基本工作 计算位置:构造散列函数直接确定关键词存储位置散列函数的设计,主要目的是构造随机性:计算简...

  • 散列表

    散列表是一种基本的数据结构,那么散列表到底是什么样的一种数据结构呢?又有哪些应用场景呢? 假如我们要从一本电话本中...

  • 散列表

    散列表 认识散列表 是 字典(键 、值对)的一种实现方式。每次在字典中获取一个值,都需要重复遍历字典,如果用散列表...

  • 散列表

    散列函数将被查找的键转换为数组的索引 解决冲突的方法:拉链法和线性探测法 将整数散列最常见的方法是除留余数法,通常...

网友评论

      本文标题:散列表

      本文链接:https://www.haomeiwen.com/subject/bgiqnhtx.html