Hash表有:
布隆过滤器、bitmap、HashSet、HashMap、ConcurrentHashMap
JDK的HashMap
HashMap不能由get是否为空来判断是否存在某个键
如果key存在,那么覆盖,不存在则存入整个键值对。
HashMap的一些参数
默认初始化长度为16
默认负载因子0.75
当table被使用75%以上时会二倍扩容,是效率和空间的折中,比如ArrayList扩容1.5因为扩容代价比较小,HashMap需要reHash等等。
数组中链表超过8转红黑树,为什么是8?
- 这一段注释的内容和目的都是为了解释在java8 HashMap中引入Tree Bin(也就是放入数据的每个数组bin从链表node转换为red-black tree node)的原因
- TreeNode虽然改善了链表增删改查的性能,但是其节点大小是链表节点的两倍
- 虽然引入TreeNode但是不会轻易转变为TreeNode(如果存在大量转换那么资源代价比较大),根据泊松分布来看转变是小概率事件,性价比是值得的
- 泊松分布是二项分布的极限形式,两个重点:事件独立、有且只有两个相互对立的结果
- 泊松分布是指一段时间或空间中发生成功事件的数量的概率
- 对HashMap table[]中任意一个bin来说,存入一个数据,要么放入要么不放入,这个动作满足二项分布的两个重点概念
- 对于HashMap.table[].length的空间来说,放入0.75*length个数据,某一个bin中放入节点数量的概率情况如上图注释中给出的数据(表示数组某一个下标存放数据数量为0~8时的概率情况)
- 举个例子说明,HashMap默认的table[].length=16,在长度为16的HashMap中放入12(0.75*length)个数据,某一个bin中存放了8个节点的概率是0.00000006
- 扩容一次,16*2=32,在长度为32的HashMap中放入24个数据,某一个bin中存放了8个节点的概率是0.00000006
- 再扩容一次,32*2=64,在长度为64的HashMap中放入48个数据,某一个bin中存放了8个节点的概率是0.00000006
所以,当某一个bin的节点大于等于8个的时候,就可以从链表node转换为treenode,其性价比是值得的。
理解:
- 链表占据的空间小一半:在1-8的时候使用链表,那么时间复杂度其实是O1的,不仅数据结构占用空间是红黑树的一半;
- 红黑树在数量较少时插入耗时:链表插入简单,根据编写人员给的数据,数据少的时候冲突时的插入发生概率较大,红黑树会有平衡,改色等操作,远远没有链表来的快速。
- 但是在大量数据冲突的时候,链表的时间复杂度就会达到O(N),尤其在Hash函数较差的时候,所以使用红黑树来有效降低高数据的查询O(logN)。
/* 默认参数,在Hash函数理想的情况下, 每个位置上Hash冲突发生的数据个数。
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006*/
关于两个Node结构空间的源码
//摘自ConcurrentHashMap源码,上方是Node,下方是TreeNode
//看起来没差多少,但是TreeNode其实是继承了Node的,所以父类的成员变量也是有的。
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
hash的处理
hashCode
hashCode 表示对象在 hash 表中的位置,对于同一个对象来说,多次调用,返回相同的 hashCode。
- 如果 Object.equal () 相等,Object.hashCode () 也必然相等。重写时也建议保证此特性。
- 如果 Object.equal () 不相等,这并不要求 Object.hashCode () 也返回不同值。如果真出现这种情况,最好优化代码,充分利用 hash 表的性能。
重写hashCode:如果你重载了equals,比如说是基于对象的内容实现的,而保留hashCode的实现不变,那么很可能某两个对象明明是“相等”,而hashCode却不一样。
确定位置
数组结构内通过(n-1)&hash
取余,取hash值对于当前容量的余。
Key–Hash—取模—>存储
判断key有三个部分
((k = p.key) == key || (key != null && key.equals(k)))
时间复杂度
O1,On,Ologn(红黑树)
Hashmap缺点:
速度快;但是存不了大数据,线程不安全的,扩容的时候会用空间的操作,不支持多线程,主要是put的时候。
HashTable:
线程安全。继承自Dictionary类实现了Map。方法是同步的,而HashMap中的方法缺省情况下是非同步的。
key和value都不能有null值。
ConcurrentHashMap:
分段锁(JDK1.7),默认16个分段锁。
JDK1.8 使用CAS加synchronized
Hash(散列函数)
数组是简单的Hash,但没有数组就没有散列表,数组的散列函数为:y=x。
- 直接寻址法
- 余数法
- 数字分析法
- 平方取中法
- 折叠法
- 随机数法
- …
Java中的Hash函数
int hash(Object key){
int h = key.hashCode();
return (h^(h>>>16))&(capacity - 1);//cap为散列表大小
}
Hash冲突
两种方法解决Hash冲突
探测(线性寻址)+链表(拉链)
开放寻址:
如果出现散列冲突,就重新探测一个空闲位置,放入。
Hash函数不变,在寻找对应值时,额外进行比较,如果值不对应或者有delete
标志,再向下探测
装不下:扩容
链路地址(更常用):
寻址后找到的是链表,即不存值,存链表指针 。
链表插入、删除快,但是查询慢。二叉搜索树可能会退化成链表,所以加入了红黑树。