彻底理解HashMap的元素插入原理

转载自   彻底理解HashMap的元素插入原理

HashMap,是Java语言中比较基础也比较重要的一种数据结构,由于其用途广泛,所以,Java的工程师在设计HashMap的时候考虑了很多因素。

通过阅读HashMap的源码,可以学习到很多知识,本文就是一篇基于HashMap源码的深度分析。

全文文字+代码大概有1.5W左右,阅读时间大概半小时。如果你没有完整的半个小时时间,请先收藏,欢迎转发。

关于HashMap,还有几篇文章可以结合着一起看:

全网把Map中的hash()分析的最透彻的文章,别无二家。

关于HashMap容量的初始化,还有这么多学问。

 

HashMap

     作为哈希表的Map接口实现,其具备以下几个特点:

  1. 和HashTable类似,采用数组+单链表形式存储元素,从jdk1.8开始,增加了红黑树的结构,当单链表中元素个数超过指定阈值,会转化为红黑树结构存储,目的就是为了解决单链表元素过多时查询慢的问题。

  2. 和HashTable不同的是,HashMap是线程不安全的,方法都未使用synchronized关键字。因为内部实现不同,允许key和value值为null。

  3. 构建HashMap实例时有两个重要的参数,会影响其性能:初始大小和加载因子。初始大小用来规定哈希表数组的长度,即桶的个数。加载因子用来表示哈希表元素的填满程度,越大则表示允许填满的元素就越多,哈希表的空间利用率就越高,但是冲突的机会也就增加了。反之,越小则冲突的机会就会越少,但是空间很多就浪费了。

 

静态常量

1、源码:

 /*** 默认初始大小,值为16,要求必须为2的幂*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** 最大容量,必须不大于2^30*/static final int MAXIMUM_CAPACITY = 1 << 30;/*** 默认加载因子,值为0.75*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** hash冲突默认采用单链表存储,当单链表节点个数大于8时,会转化为红黑树存储*/
static final int TREEIFY_THRESHOLD = 8;/*** hash冲突默认采用单链表存储,当单链表节点个数大于8时,会转化为红黑树存储。* 当红黑树中节点少于6时,则转化为单链表存储*/
static final int UNTREEIFY_THRESHOLD = 6;/*** hash冲突默认采用单链表存储,当单链表节点个数大于8时,会转化为红黑树存储。* 但是有一个前提:要求数组长度大于64,否则不会进行转化*/
static final int MIN_TREEIFY_CAPACITY = 64;

     注意:HashMap默认采用数组+单链表方式存储元素,当元素出现哈希冲突时,会存储到该位置的单链表中。但是单链表不会一直增加元素,当元素个数超过8个时,会尝试将单链表转化为红黑树存储。但是在转化前,会再判断一次当前数组的长度,只有数组长度大于64才处理。否则,进行扩容操作。此处先提到这,后续会有详细的讲解。

2、问题:

     问:为何加载因子默认为0.75?
     答:通过源码里的javadoc注释看到,元素在哈希表中分布的桶频率服从参数为0.5的泊松分布,具体可以参考下StackOverflow里的解答:https://stackoverflow.com/questions/10901752/what-is-the-significance-of-load-factor-in-hashmap

 

 

构造函数

1、无参构造函数:

public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

2、带参构造函数,指定初始容量:

public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

3、带参构造函数,指定初始容量和加载因子:

3.1、源码:

 public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity)//通过后面扩容的方法知道,该值就是初始创建数组时的长度
}//返回大于等于cap最小的2的幂,如cap为12,结果就是16
static final int tableSizeFor(int cap) {int n = cap - 1;//为了保证当cap本身是2的幂的情况下,能够返回原本的数,否则返回的是cap的2倍n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

3.2、示例:

下面我们以cap等于8为例:

不减一的过程如下:

图注:tableSizeFor不减一过程

最后执行加1操作,那么返回的是2^4=16,是cap的2倍。

减一的过程如下:

 

图注:tableSizeFor减一过程


最后执行加1操作,那么返回的是2^3=8,也就是cap本身。

3.3、问题:

     问:为何数组容量必须是2次幂?
     答:索引计算公式为i = (n - 1) & hash,如果n为2次幂,那么n-1的低位就全是1,哈希值进行与操作时可以保证低位的值不变,从而保证分布均匀,效果等同于hash%n,但是位运算比取余运算要高效的多。

4、带参构造函数,指定Map集合:

 public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {if (table == null) { // pre-sizefloat ft = ((float)s / loadFactor) + 1.0F;int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);if (t > threshold)threshold = tableSizeFor(t);}else if (s > threshold)resize();for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();putVal(hash(key), key, value, false, evict);}}
}

 

添加元素

1、源码:

 public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}//将key的哈希值,进行高16位和低16位异或操作,增加低16位的随机性,降低哈希冲突的可能性static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//首次table为null,首先通过resize()进行数组初始化if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//利用index=(n-1)&hash的方式,找到索引位置//如果索引位置无元素,则创建Node对象,存入数组该位置中if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {  //如果索引位置已有元素,说明hash冲突,存入单链表或者红黑树中Node<K,V> e; K k;//hash值和key值都一样,则进行value值的替代if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode) //hash值一致,key值不一致,且p为红黑树结构,则往红黑树中添加e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else { //hash值一致,key值不一致,且p为单链表结构,则往单链表中添加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) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold) //当元素个数大于新增阈值,则通过resize()扩容resize();afterNodeInsertion(evict);return null;
}

2、流程图:

 

图注:添加元素流程图

3、hash计算:

     问:获取hash值时:为何在hash方法中加上异或无符号右移16位的操作?
     答:此方式是采用"扰乱函数"的解决方案,将key的哈希值,进行高16位和低16位异或操作,增加低16位的随机性,降低哈希冲突的可能性。

     下面我们通过一个例子,来看下有无"扰乱函数"的情况下,计算出来索引位置的值:

 

图注:hash计算

 

 

扩容

1、源码:

 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) { //当前长度超过MAXIMUM_CAPACITY,新增阈值为Integer.MAX_VALUEthreshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY) //进行2倍扩容,如果当前长度超过初始16,新增阈值也做2倍扩容newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // 数组为空,指定了新增阈值newCap = oldThr;else { //数组为空,未指定新增阈值,采用默认初始大小和加载因子,新增阈值为16*0.75=12newCap = 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) { //哈希值和原数组长度进行&操作,为0则在原数组的索引位置,非0则在原数组索引位置+原数组长度的新位置if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;
}

2、流程图:

2.1 首次调用扩容方法:

 

图注:首次调用扩容方法

2.2 示例:

情况一:

  1. 使用无参构造函数:

HashMap<String, Integer> hashMap = new HashMap<>();
  1. put元素,发现table为null,调用resize扩容方法:

int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
  1. oldCap为0,oldThr为0,执行resize()里的该分支:

newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
  1. newCap为16,newThr为12,也就是说HashMap默认数组长度为16,元素添加阈值为12。

  2. threshold为12。创建大小为16的数组,赋值给table。

情况二:

使用有参构造函数:

HashMap<String, Integer> hashMap = new HashMap<>(7);

oldCap为0,oldThr为8,执行resize()里的该分支:

else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;

newCap为8,newThr为0,执行resize()里的该分支:

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;

threshold为6。创建大小为8的数组,赋值给table。

2.3 非首次调用扩容方法:

 

图注:非首次调用扩容方法

2.4 示例:

接着2.2里的情况二,继续添加元素,直到扩容:

oldCap为8,oldThr为6,执行resize()里的该分支:

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; // double threshold
}

oldCap小于MAXIMUM_CAPACITY,进行2倍扩容,newCap为16。oldCap小于DEFAULT_INITIAL_CAPACITY,不做newThr的扩容,为0,执行resize()里的该分支:

 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;
..........省略.......//将原数组元素存入新数组中

因为newCap小于MAXIMUM_CAPACITY ,ft为newCap*加载因子为12,threshold为12。创建大小为16的数组,赋值给table,并将原数组元素放入新数组中。

继续添加元素,直到扩容:

oldCap为16,oldThr为12,执行resize()里的该分支:

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; // double threshold
}

oldCap小于MAXIMUM_CAPACITY,将数组长度进行2倍扩容,newCap为32。oldCap>=DEFAULT_INITIAL_CAPACITY,将添加元素的阈值也进行2倍扩容,注意此时不再用加载因子去计算阈值,而是随着数组长度进行相应的2倍扩容,threshold为24。

创建大小为32的数组,赋值给table,并将原数组元素放入新数组中。

threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
..........省略.......//将原数组元素存入新数组中

继续添加元素,扩容到数组长度等于MAXIMUM_CAPACITY:

oldCap为MAXIMUM_CAPACITY,执行resize()里的该分支:

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; // double threshold
}

因为oldCap等于MAXIMUM_CAPACITY,threshold设置为 Integer.MAX_VALUE,不再扩容,直接返回原数组。此时继续添加元素,Integer.MAX_VALUE+1=Integer.MIN_VALUE,不再大于threshold,则不再进行扩容操作了。

 

 

树化操作

1、源码:

将原本的单链表转化为双向链表,再遍历这个双向链表转化为红黑树:

 final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;//树形化还有一个要求就是数组长度必须大于等于64,否则继续采用扩容策略if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;//hd指向首节点,tl指向尾节点do {TreeNode<K,V> p = replacementTreeNode(e, null);//将链表节点转化为红黑树节点if (tl == null) // 如果尾节点为空,说明还没有首节点hd = p;  // 当前节点作为首节点else { // 尾节点不为空,构造一个双向链表结构,将当前节点追加到双向链表的末尾p.prev = tl; // 当前树节点的前一个节点指向尾节点tl.next = p; // 尾节点的后一个节点指向当前节点}tl = p; // 把当前节点设为尾节点} while ((e = e.next) != null); // 继续遍历单链表//将原本的单链表转化为一个节点类型为TreeNode的双向链表if ((tab[index] = hd) != null) // 把转换后的双向链表,替换数组原来位置上的单向链表hd.treeify(tab); // 将当前双向链表树形化}
}

将双向链表转化为红黑树的具体实现:

 final void treeify(Node<K,V>[] tab) {TreeNode<K,V> root = null;  // 定义红黑树的根节点for (TreeNode<K,V> x = this, next; x != null; x = next) { // 从TreeNode双向链表的头节点开始逐个遍历next = (TreeNode<K,V>)x.next; // 头节点的后继节点x.left = x.right = null;if (root == null) {x.parent = null;x.red = false;root = x; // 头节点作为红黑树的根,设置为黑色}else { // 红黑树存在根节点K k = x.key; int h = x.hash;Class<?> kc = null;for (TreeNode<K,V> p = root;;) { // 从根开始遍历整个红黑树int dir, ph;K pk = p.key;if ((ph = p.hash) > h) // 当前红黑树节点p的hash值大于双向链表节点x的哈希值dir = -1;else if (ph < h) // 当前红黑树节点的hash值小于双向链表节点x的哈希值dir = 1;else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0) // 当前红黑树节点的hash值等于双向链表节点x的哈希值,则如果key值采用比较器一致则比较key值dir = tieBreakOrder(k, pk); //如果key值也一致则比较className和identityHashCodeTreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { // 如果当前红黑树节点p是叶子节点,那么双向链表节点x就找到了插入的位置x.parent = xp;if (dir <= 0) //根据dir的值,插入到p的左孩子或者右孩子xp.left = x;elsexp.right = x;root = balanceInsertion(root, x); //红黑树中插入元素,需要进行平衡调整(过程和TreeMap调整逻辑一模一样)break;}}}}//将TreeNode双向链表转化为红黑树结构之后,由于红黑树是基于根节点进行查找,所以必须将红黑树的根节点作为数组当前位置的元素moveRootToFront(tab, root);
}

将红黑树的根节点移动到数组的索引所在位置上:

 static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {int n;if (root != null && tab != null && (n = tab.length) > 0) {int index = (n - 1) & root.hash; //找到红黑树根节点在数组中的位置TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; //获取当前数组中该位置的元素if (root != first) { //红黑树根节点不是数组当前位置的元素Node<K,V> rn;tab[index] = root;TreeNode<K,V> rp = root.prev;if ((rn = root.next) != null) //将红黑树根节点前后节点相连((TreeNode<K,V>)rn).prev = rp;if (rp != null)rp.next = rn;if (first != null) //将数组当前位置的元素,作为红黑树根节点的后继节点first.prev = root;root.next = first;root.prev = null;}assert checkInvariants(root);}
}

 

 

红黑树插入

1、源码:

 final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {Class<?> kc = null;boolean searched = false;TreeNode<K,V> root = (parent != null) ? root() : this;for (TreeNode<K,V> p = root;;) {int dir, ph; K pk;if ((ph = p.hash) > h)//进行哈希值的比较dir = -1;else if (ph < h)dir = 1;else if ((pk = p.key) == k || (k != null && k.equals(pk)))return p;else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0) {//hash值相同,则按照key进行比较if (!searched) {TreeNode<K,V> q, ch;searched = true;if (((ch = p.left) != null &&(q = ch.find(h, k, kc)) != null) ||//去左子树中查找哈希值相同,key相同的节点((ch = p.right) != null &&(q = ch.find(h, k, kc)) != null))//去右子树中查找哈希值相同,key相同的节点return q;}dir = tieBreakOrder(k, pk);//通过比较k与pk的hashcode}TreeNode<K,V> xp = p;if ((p = (dir <= 0) ? p.left : p.right) == null) {//找到红黑树合适的位置插入Node<K,V> xpn = xp.next;TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);if (dir <= 0) //插入到左节点或者右节点xp.left = x;elsexp.right = x;xp.next = x;//插入到双向链表合适的位置x.parent = x.prev = xp;if (xpn != null)((TreeNode<K,V>)xpn).prev = x;moveRootToFront(tab, balanceInsertion(root, x));//做插入后的平衡调整 将平衡后的红黑树节点作为数组该位置的元素return null;}}
}

2、说明:

     当hash冲突时,单链表元素个数超过树化阈值(TREEIFY_THRESHOLD)后,转化为红黑树存储。之后再继续冲突,则就变成往红黑树中插入元素了。关于红黑树插入元素,请看我之前写的文章:TreeMap之元素插入

 

 

红黑树拆分

1、源码:

将红黑树按照扩容后的数组,重新计算索引位置,并且拆分后的红黑树还需要判断个数,从而决定是做去树化操作还是树化操作:

 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {TreeNode<K,V> b = this;// Relink into lo and hi lists, preserving orderTreeNode<K,V> loHead = null, loTail = null; //保存在原索引的红黑树TreeNode<K,V> hiHead = null, hiTail = null; //保存在新索引的红黑树int lc = 0, hc = 0;for (TreeNode<K,V> e = b, next; e != null; e = next) {next = (TreeNode<K,V>)e.next;e.next = null;if ((e.hash & bit) == 0) { //哈希值和原数组长度进行&操作,为0则在原数组的索引位置,非0则在原数组索引位置+原数组长度的新位置if ((e.prev = loTail) == null)loHead = e;elseloTail.next = e;loTail = e;++lc;}else {if ((e.prev = hiTail) == null)hiHead = e;elsehiTail.next = e;hiTail = e;++hc;}}if (loHead != null) {if (lc <= UNTREEIFY_THRESHOLD) //当红黑树的节点不大于去树化阈值,则将原索引处的红黑树进行去树化操作tab[index] = loHead.untreeify(map); //红黑树根节点作为原索引处的元素  else { //当红黑树的节点大于去树化阈值,则将原索引处的红黑树进行树化操作tab[index] = loHead;if (hiHead != null) // (else is already treeified)loHead.treeify(tab);}}if (hiHead != null) {if (hc <= UNTREEIFY_THRESHOLD) //当红黑树的节点不大于去树化阈值,则将新索引处的红黑树进行去树化操作tab[index + bit] = hiHead.untreeify(map); //红黑树根节点作为新索引处的元素else { //当红黑树的节点大于去树化阈值,则将新索引处的红黑树进行树化操作tab[index + bit] = hiHead;if (loHead != null)hiHead.treeify(tab);}}
}

 

 

去树化操作

1、源码:

遍历红黑树,还原成单链表结构:

 final Node<K,V> untreeify(HashMap<K,V> map) {Node<K,V> hd = null, tl = null;for (Node<K,V> q = this; q != null; q = q.next) {  //遍历红黑树,依次将TreeNode转化为Node,还原成单链表形式Node<K,V> p = map.replacementNode(q, null);if (tl == null)hd = p;elsetl.next = p;tl = p;}return hd;
}

 

 

综合示例

1、代码:

 //插入38个元素,无hash冲突,依次存入索引0~37的位置HashMap<Integer, Integer> hashMap = new HashMap<>(64);for(int i=0; i<38; i++){hashMap.put(i, i);}//依次插入64、128、182、256、320、384,448,索引位置为0,出现hash冲突,往单链表中插入for (int i=1; i <= 7; i++) {hashMap.put(64*i, 64*i);}
//插入512,hash冲突,往单链表中插入。此时单链表个数大于TREEIFY_THRESHOLD,将单链表转化为红黑树
hashMap.put(64*8, 64*8);
//插入576,hash冲突,往红黑树中插入
hashMap.put(64*9, 64*9);
//hash不冲突,保存到数组索引为38的位置,此时总元素个数为48,新增阈值为48,不做处理。
hashMap.put(38, 38);
//hash不冲突,保存到数组索引为39的位置,此时总元素个数为49,新增阈值为48,扩容!!!
hashMap.put(39, 39);

2、内部实现过程:

 

图注:添加0~37

 

图注:添加64~448

 

图注:添加512

 

图注:树化操作

 

图注:调整根的位置

 

 

图注:添加576

 

图注:添加38

 

图注:添加39

 

图注:扩容

 

图注:去树化

 

 

讨论题

  1. 为何单链表转化为红黑树,要求节点个数大于8?

  2. 为何转化为红黑树前,要求数组总长度要大于64?

  3. 为何红黑树转化为单链表,要求节点个数小于等于6?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/325602.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用C#操作XML文件

今天更新一篇技术文章&#xff0c;使用C#实现对XML的操作&#xff1a;首先需要准备一个测试的XML文件&#xff0c;我这边命名为test.xml:文件内容为&#xff1a;<test><id>1</id><name>张三</name><age>18</age><id>2</id&…

Linux使用Jexus托管Asp.Net Core应用程序

第一步 安装.Net Core环境 安装 dotnet 环境参见官方网站 https://www.microsoft.com/net/core。 选择对应的系统版本进行安装。安装完成过后 输入命令查看版本&#xff0c;目前最新版为 1.04&#xff1a; dotnet --version 此时已经可以发布Asp.Net Core应用程序到Linux上…

C++字符串分割替换 ubuntu版本

#include <iostream> #include <string> #include <vector> using namespace std; vector<string> mySplit(const string& str,string sp_string) // split(),str 是要分割的string { vector<string> vecString; int sp_stringLen sp_st…

优秀学生专栏——董超

优秀学生--董超今天回访了下17级优秀学生董超同学&#xff0c;董超同学在校期间一直担任小组组长&#xff0c;平时学习刻苦认真&#xff0c;各个阶段的项目也做的非常优秀&#xff0c;今年5月份左右毕业&#xff0c;所在岗位是开发&#xff0c;目前的薪资在5000左右&#xff0c…

高级开发必须理解的Java中SPI机制

转载自 高级开发必须理解的Java中SPI机制 本文通过探析JDK提供的&#xff0c;在开源项目中比较常用的Java SPI机制&#xff0c;希望给大家在实际开发实践、学习开源项目提供参考。 SPI是什么 SPI全称Service Provider Interface&#xff0c;是Java提供的一套用来被第三方实…

JS中的延时调用

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title><script type"text/javascript">var num 1;//开启一个定时器/*setInterval(function(){console.log(num);},3000);*//** 延时调用&#xff…

动态代理JDK于cglib

静态代理的缺点&#xff1a; 1、由于静态代理中的代理类是针对某一个类去做代理的&#xff0c;那么假设一个系统中有100个Service&#xff0c;则需要创建100个代理类 2、如果一个Service中有很多方法需要事务&#xff08;增强动作&#xff09;&#xff0c;发现代理对象的方法中…

以深圳.NET俱乐部名义 的技术交流会圆满成功

2017年5月13日的深圳下着暴雨&#xff0c;一场以深圳.NET俱乐部名义的.NET技术交流会在微软Build 2017刚闭幕时在罗湖布吉路与翠山路交界处富基PARK国际6F举办&#xff0c;这次交流以微软Build 2017 大会发布的.NET Standard 2.0 Preview1/.NET Core 2.0 Preview 1为契机&#…

C#中的序列化和反序列化

序列化&#xff1a;是将对象的状态存储到特定存储介质的过程&#xff0c;也可以说是将对象状态转换为可保持或传输的格式的过程。 上面的解释是官方定义&#xff0c;大白话解释就是&#xff0c;将对象以二进制的方式存储在文件中&#xff0c;如果简简单单的将一些数据或者内容存…

JS中的JSON

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title><!--如果需要兼容IE7及以下的JSON操作&#xff0c;则可以通过引入一个外部的js文件来处理--><script type"text/javascript" src"js…

关于勒索病毒 Ransom:Win32.WannaCrypt 解决方案的最后一次说明

2017/5/12 晚&#xff0c;勒索软件 Ransom:Win32.WannaCrypt 大面积暴发。比病毒爆发更火的&#xff0c;则是各类关于此病毒的新闻、解决方法在朋友圈等社交媒体的爆发。 其中&#xff0c;有主观善意但客观一知半解的指导&#xff0c;更有夹带私货的安全软件商携各类工具的广告…

maven的三种packaging方式

pom是maven依赖文件 jar是java普通项目打包 war是java web项目打包 pom&#xff1a;打出来可以作为其他项目的maven依赖&#xff0c;在工程A中添加工程B的pom&#xff0c;A就可以使用B中的类。用在父级工程或聚合工程中。用来做jar包的版本控制。 jar包&#xff1a;通常是开发…

C#中的序列化和反序列化案例

序列化&#xff1a;是将对象的状态存储到特定存储介质的过程&#xff0c;也可以说是将对象状态转换为可保持或传输的格式的过程。上面的解释是官方定义&#xff0c;大白话解释就是&#xff0c;将对象以二进制的方式存储在文件中&#xff0c;如果简简单单的将一些数据或者内容存…

浅谈MySQL的B树索引与索引优化

转载自 浅谈MySQL的B树索引与索引优化 MySQL的MyISAM、InnoDB引擎默认均使用B树索引&#xff08;查询时都显示为“BTREE”&#xff09;&#xff0c;本文讨论两个问题&#xff1a; 为什么MySQL等主流数据库选择B树的索引结构&#xff1f; 如何基于索引结构&#xff0c;理解常…

.NET特性:异步流

自从VB/C#开始支持async/await后&#xff0c;开发者一直在期待异步版本的IEnumerable。但直到C# 7和ValueTask发布前&#xff0c;从性能的角度来看这一要求几乎是不可能实现的。 在老版本C#中&#xff0c;开发者每次使用await时都需要进行内存分配。如果要枚举10,000个项&…

MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established

Establishing SSL connection without server’s identity verification is not recommended. According to MySQL 5.5.45, 5.6.26 and 5.7.6 requirements SSL connection must be established by default if explicit option isn’t set. For compliance with existing appli…

优秀学生专栏——孙珩发

继优秀学生董超同学之后的孙珩发同学的回访录&#xff0c;孙珩发同学于今年5月份毕业&#xff0c;是一个非常非常懂事的孩子&#xff0c;比如让他帮忙拿一下水杯&#xff0c;一般的同学都是直接给你拿杯子过来&#xff0c;而孙珩发同学可不是&#xff0c;他会将水杯里面接满水&…

Java并发编程包中atomic的实现原理

转载自 Java并发编程包中atomic的实现原理 这是一篇来自粉丝的投稿&#xff0c;作者【林湾村龙猫】最近在阅读Java源码&#xff0c;这一篇是他关于并发包中atomic类的源码阅读的总结。Hollis做了一点点修改。 引子 在多线程的场景中&#xff0c;我们需要保证数据安全&#…

优秀学生专栏——王浩

今天继续回访优秀学生王浩&#xff0c;王浩是班级里学习最好的同学&#xff0c;就业的时候也是最早入职的&#xff0c;目前所处岗位是开发&#xff0c;最近在北京出差。企业多次向学校表扬王浩同学&#xff0c;以下是王浩同学的简单回访&#xff1a;想对学弟学妹说些什么&#…