哈希表
(hash table) 也叫散列表
,是一种非常重要的数据结构。
应用场景及其丰富,许多缓存技术
(比如memcached)的核心其实就是在内存中维护一张大的哈希表,
关注博主不迷路,获取更多干货资源
我画的visio原图
链接,有兴趣的同学可以在这个图上自己改
| 链接:https://pan.baidu.com/s/12Rx9pp2kBje2E46eayuRfw 提取码:6666 复制这段内容后打开百度网盘手机App,操作更方便哦--来自百度网盘超级会员V4的分享
|
1 基础
1.1 数组、链表和散列表
1.1.1 数组
优势
:内存连续,查询效率高
劣势
:插入效率低。大小固定,不可动态存储。
1.1.2 链表
优势
:插入效率高
劣势
:查询效率低
1.1.3 散列表
散列表:数组+链表
1.2 什么是哈希
Hash也称散列、哈希。基本原理是把任意长度的输入,通过Hash算法变成固定长度的输出.
这个映射的规则就是哈希算法
,原始数据映射后的二进制串
就是哈希值
.
1.2.1 哈希的特点
1.从hash值不可以反向推出
原始数据
2.输入数据的微小变化
会得到完全不同的hash值,相同的数据会得到相同的hash值。
3.hash算法的执行效率要高效
,长的文本也要快速算出哈希值。
4.hash算法的冲突概率
要小。
2 HashMap原理分析
2.1 HashMap存储原理
2.2 PutVal方法原理
2.3 Resize方法原理
3 手撕源码
3.1 常量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
|
3.2 变量
| transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
|
3.3 put方法
3.3.1 put方法
| public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
|
3.3.2 hash方法
可以看到key为null的值都放在数组索引0的位置,也就是HashMap可以存key为null的数据
,但是这个数据只有一个。
| static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
|
3.3.3 putVal方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
|
3.4 resize扩容方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; } else if (oldThr > 0) newCap = oldThr; else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
|
4 为什么选用红黑树
红黑树是一种近乎平衡的二叉查询树
二叉查询树最差的时间复杂度是0(n),也就是数据是一条链的时候,这个肯定不适合
二叉平衡树的话,查询性能很好 0(logn),但是几乎每次插入数据,都会破坏平衡性,从而产生自平衡操作,很浪费性能。
红黑树是二叉平衡树的变体,保证了近乎平衡的同时,插入数据不会频繁影响平衡性,不会频繁的进行自平衡操作,不像二叉平衡树那样。
5 HashMap 为什么是线程不安全的
5.1 插入数据的时候
AB线程同时插入,两个计算除了相同的哈希值,对应数组的相同位置,第一个线程写了数据后,第二个线程再写,不就覆盖了。
5.2 扩容的时候
第一步:线程2已经将1和2元素弄好了,这时候挂起
第二步:线程1将弄好。
第三步:线程2找到2节点的next,发现是1,然后把1插进去,就是1指向2,如图中红色箭头,死循环了。
5.3 ……
关注博主不迷路