在刷leetcode的算法题时,HashMap须要大量使用,并且也是面试的高频问题。这里记录了HashMap一些增、删、改、查的实现细节和时间复杂度,罗列了一些比较有用的方法,以及其它的一些细节。html
一、底层数据结构
HashMap在jdk1.7及以前的版本中,由数组+链表的结构实现,从jdk1.8开始,由数组+链表+红黑树的结构实现,这里在jdk1.8的基础上探讨HashMap。
源码中维护了一个数组:java
1 transient Node<K,V>[] table; 2 static class Node<K,V> implements Map.Entry<K,V> { 3 final int hash; 4 final K key; 5 V value; 6 Node<K,V> next; 7 }
这个数组存储的Node,就包含了咱们put时的K与V,K的hash值,以及指向下一个节点的指针next。数组中查询节点的时间复杂度是O(1),可是插入、删除的时间复杂度是O(n),因此执行插入和删除操做比较耗时。HashMap中加入链表结构来解决这个问题。咱们知道,解决hash冲突的通常方法有:开发地址法、二次hash法、拉链法等,这里采用的就是拉链法,也就是这里的数组+链表结构了。查找元素时,最好的状况是就在数组中,时间复杂度为O(1),最坏的状况是在链表的末尾,时间复杂度是O(n)(固然,因为HashMap的扩容机制和良好的hash算法,hash冲突发生得比较少);插入和删除的时间复杂度就变成了O(1)了。面试
jdk1.8加入了红黑树,当链表的长度达到8的时候就会由链表升维为红黑树,当红黑树减小到6时又由红黑树降到链表。这里须要补充一点的是,红黑树的节点占用的空间比链表要大,维护红黑树的空间成本比较大,但操做方便;而链表正好相反,因此这里的8和6是一个平衡的值。在链表转为红黑树时,还会判断当前的Entry的数量是否小于64,小于64时会扩容,减小hash冲突,生成红黑树的可能性就小了不少。可见,只有当数量比较多时,维护红黑树的效率才比较明显。算法
红黑树的节点以下,实际上也Node的子类:数组
1 static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> { 2 TreeNode<K,V> parent; // red-black tree links 3 TreeNode<K,V> left; 4 TreeNode<K,V> right; 5 TreeNode<K,V> prev; // needed to unlink next upon deletion 6 boolean red; 7 }
二、构造函数的选择
HashMap提供了4个构造函数,实际工做中可能会用到下面3个:数据结构
1 public HashMap() { 2 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 3 } 4 public HashMap(int initialCapacity) { 5 this(initialCapacity, DEFAULT_LOAD_FACTOR); 6 } 7 public HashMap(Map<? extends K, ? extends V> m) { 8 this.loadFactor = DEFAULT_LOAD_FACTOR; 9 putMapEntries(m, false); 10 }
这三个构造函数都使用了默认的扩容因子,函数
static final float DEFAULT_LOAD_FACTOR = 0.75f;
其值为0.75,当HashMap当前使用率达到整个容量(capacity)的75%时就会扩容。第一个构造函数使用得最频繁,会分配默认大小的容量:性能
1 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
第二个构造函数会指定初始容量,指定容量后经过计算,会分配比该初始值大的最近的2的n次方大小的容量,好比传入的initialCapacity为12,实际上会分配16的容量,最大能分配的容量为;this
1 static final int MAXIMUM_CAPACITY = 1 << 30;
第三个能够用于复制指定的HashMap。因为扩容须要执行很多操做,因此确定是会占用一些资源的,若是平时开发比较明确须要使用多少容量,最好使用第二个,能够避免频繁扩容影响性能。spa
三、元素的插入
插入元素的方法是put(K,V),其基本步骤是:
(1)根据Key算出hash值,(n-1)&hash来肯定其在数组中的index(这里的n表示数组的长度)
(2)若是数组的这个index位置为空,则直接插入,时间复杂度是O(1),若是达到扩容条件还会扩容。
(3)若是数组的这个index已经有值了,那就依次遍历,比价Key来判断是否已经存在,存在就修改该节点的Value,不存在就新建节点并插在链尾。
若是链表长度达到了8,此时会升维造成红黑树。若是还在链表阶段,时间复杂度是O(1)+O(k),这里O(1)是插入,O(k)是遍历,因为不会超过8,因此也能够认为是O(1)。在造成红黑树时,还会判断容量是否小于64,若是是,会扩容。
(4)在第3步中,可能插入前已是红黑树了,那就在红黑树中先查找是否存在,存在则修改,不存在则新建并插入。这样,时间复杂度是O(l)+O(logK)。因此综合来看,能够理解为插入一个元素时时间复杂度最好是O(1),最坏是O(logn)
四、获取元素
获取元素的方法是get(K),基本步骤是:
(1)根据Key的hash值肯定其在数组中的index。
(2)先判断数组的这个地方是否有节点,没有则返回null。
(3)若是有,则根据hash和Key判断第一个节点是否为目标节点,是则返回其Value。不然继续判断,根据第一个节点是TreeNode实例来判断当前是链表仍是红黑树。 一样根据hash值和Key来肯定是否存在,存在则返回Value,不然返回null。因此时间复杂度也和插入时相似,最好时是O(1),最坏时是O(logn)。
五、删除元素
删除元素的方法是remove(K),先和获取元素同样查找该节点,删除,而后调整结构。
六、Key为null时的处理
HashMap的K和V都可觉得null,当Key为null时有,其hash值定为0;
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); 3 } 4 static final int hash(Object key) { 5 int h; 6 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 7 }
七、作算法题时经常使用的方法
1 Map<Object, Object> map = new HashMap<>(); 2 map.put(K,V); //存取KV对 3 map.get(K); //若是不存在,则返回null 4 map.getOrDefault(K,defaultValue); //相比get方法,会获得设定的默认值defaultValue。该方法颇有用 5 map.entrySet(); //获取全部KV对的实体Set,其元素类型为Map.Entry<K, V>。HashMap中的Node,TreeNode都是其子类。 6 map.keySet(); //获取Key的集合Set 7 map.values(); //获取value的集合Collection,区别于Set 8 map.containsKey(K); //判断是否包含指定Key的Entry 9 map.containsValue(V); //判断是否包含指定Value的Entry 10 map.remove(K); //删除指定Key的Entry 11 map.putAll(otherMap); //复制给定的map 12 map.size(); //Entry的数量 13 map.clear(); //清除全部Entry 14 map.isEmpty(); //判断是否为空
相关阅读
https://tech.meituan.com/2016/06/24/java-hashmap.html