JDK 1.8 HashMap扩容机制

news2024/11/27 4:39:05

我们首先来看利用无参构造函数创建HashMap如何扩容。首先创建一个无参构造出来的hashmap

 HashMap hashMap = new HashMap();

该构造函数源码如下:

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

此时,该构造函数只会设置默认的加载因子,即计算阈值threshold,默认为0.75大小。此时底层的table数组是指向null的

在这里插入图片描述
接下来调用put方法,往hashmap里面存一个键值对:

hashMap.put(1, 1);

由于底层的table是指向null的,所以需要肯定进行扩容。调用put方法,该方法的源码如下:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

该方法首先会调用hash(key),来计算我们传进来的key的hash值,我们看一下hash方法的源码:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

该方法接收一个Object类型的key,即所有类型的参数都可以接收。key分为两种情况计算:

  1. 如果key为null:计算结构为0。所以hashmap可以接收key为null的键值对,这是没问题的,因为在这里做了处理,如果key为null,哈希值为0
  2. 如果key不为null:计算结果为 (h = key.hashCode()) ^ (h >>> 16)。这里相当于 h^(h>>>16),其中h是key的hashCode方法返回值。把h与h的右移16为后进行异或操作,高16位保持不变,低16位会进行扰动。

我们重点来看一下第二种情况,key不为null时,HashMap如何优化hashCode函数,使得Hash值散列更分散。以一个具体的数为例,假设h = 123456789 。下面我用32位二进制数来表述h:

  • 原始的哈希值的二进制数:           0000 0111 0101 1011 1100 1101 0001 0101
  • 无符号右移16位后的的二进制数:0000 0000 0000 0000 0000 0111 0101 1011
  • 上面两个二进制数异或的结果:    0000 0111 0101 1011 1100 1010 0100 1110

由于是异或,即相同为0,不同为1,所以与0异或,结果就是本身。而无符号右移16位,说明高16位全是0,与高16为进行异或运算,说明结果不变,而低16位的值有0也有1,与低16位异或,结果就不一样了。

这种二进制级别的位操作能有效地分散哈希值,从而在哈希表中产生更好的键分布,减少潜在的哈希冲突。

接着往下走,调用 putVal(hash(key), key, value, false, true),putVal方法。下面是该方法的源码:

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) // -1 for 1st
                            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 key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

我们首先看该方法接收的参数:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)

接收key的哈希值hash,传进来的 key,value键值对,onlyIfAbsent 是false,evict是true。后面两个参数不重要,重点是前面三个参数:

  1. hash 经过计算得到kye的哈希值
  2. key
  3. value

进入函数,该方法创建四个局部变量:

Node<K,V>[] tab;
Node<K,V> p;
int n
int i;

HashMap底层的数组table就是Node<K,V>类型的,即Node数组,p就是一个node节点。
定义完局部变量,会进行该方法里面的第一个if条件判断:

if ((tab = table) == null || (n = tab.length) == 0)

该语句首先将成员变量table赋值给局部变量tab:

tab = table

然后判断tab是不是为null,或tab的长度是不是为0。这两种情况都对应一个结果:当前数组放不了传进来的键值对,需要进行扩容,所以该if条件里面的代码是:

n = (tab = resize()).length;

这里出现了扩容核心方法resize(),它在putVal方法里面出现了两次,我们总结这两次出现的条件:

  1. 第一次是if ((tab = table) == null || (n = tab.length) == 0) 判断为真,即第一次调用put方法会触发扩容。
  2. 第二次是if (++size > threshold)判断为真,即hashmap里面的元素个数大于阈值。

在这里插入图片描述

不同条件扩容结果是不一样的,但都调用了resize方法,我们看resize方法是如何控制的。
以下是resize方法的源码:

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; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        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 { // preserve order
                    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;
}

这部分代码较长,其实可以划分为两部分,从if (oldTab != null) 处划分,上面是关于扩容后的容量计算,下面是关于扩容后元素迁移。
我们首先看第一部分:

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; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
else {               // zero initial threshold signifies using defaults
    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;

首先定义了一大堆局部变量:

  • oldTab,旧表,oldTab = table
  • oldCap,旧表容量,(oldTab == null) ? 0 : oldTab.length;,因为可能是第一次构造,所以oldTab 可能为null,这里用三元表达式计算值。
  • oldThr,旧的阈值,oldThr = threshold
  • newCap,新的容量,newCap = 0
  • newThr,新的阈值,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; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
else {               // zero initial threshold signifies using defaults
    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);
}

我们先不关注判断语句里面的执行逻辑,我们看什么情况会执行到哪个逻辑代码块里。

if (oldCap > 0) {
	// 这里面执行的扩容逻辑表示不是第一次put扩容,而是后面达到阈值的扩容
}
else if (oldThr > 0) {
	// 运行到这里面需要oldCap = 0,表示是第一次put扩容,还需要oldThr > 0,即threshold > 0
	// 说明这里面是带参构造函数构造的map的扩容,因为带参构造函数才计算threshold
}
else {               // zero initial threshold signifies using defaults
    // 这里面是无参构造函数map,第一次调用put扩容时的处理逻辑
}
if (newThr == 0) {
    // 这里面是带参构造函数构造的map的扩容
}

为什么带参构造函数HashMap 的 threshold 不为0?
这需要去看源码了:

HashMap<Object, Object> hashMap = new HashMap<>(17);

这里构造了一个HashMap,使用带参构造函数,传入initCapacity

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

发现调用了另外一个构造函数,两个参数,需要传入加载因子,这里传入了DEFAULT_LOAD_FACTOR默认记载因子,0.75。继续进入该方法:

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);
}

前面部分代码在对传入的initialCapacity 值做合法性判断,注意看最后一句代码:

this.threshold = tableSizeFor(initialCapacity);

使用tableSizeFor计算出一个不小于initialCapacity的2的幂次,然后返回给threshold ,注意看,这里threshold的值已经被计算出来了,但是table还是null,所以会出现oldTab = 0, oldThr > 0 的情况。
在这里插入图片描述
计算出了newCap之后,就需要新建一个数组了:

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

此时如果是第一次扩容,就可以直接return newTab,如果不是第一次扩容,即oldTab不为null,里面还有数据,需要迁移到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 { // preserve order
                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;
                }
            }
        }
    }
}

首先创建了一个for循环,全部的代码都在这段for循环里面:

for (int j = 0; j < oldCap; ++j)

这段for循环是变量oldTab所有槽位的元素。
定义一个局部变量,Node<K,V> e;,因为oldTab是Node类型的,e表示oldTabl每个槽位的元素。

if ((e = oldTab[j]) != null)

先让e = oldTab[j],第j个数组元素赋值给e,在判断e是否为空,如果为空,直接判断下一个槽位。如果不为空,将oldTab[j] 迁移到 newTab某个槽位处,所以这里是最关键的代码,我们看这个if里面的代码:

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 { // preserve order
    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;
    }
}

首先让oldTab[j] = null; 此时e是执行这个位置原本指向的节点的
在这里插入图片描述
紧接着就是几个if-else将e指向的该Node节点分成了三种:

  1. e.next == null,就是单个节点,没有形成链表。
  2. e instanceof TreeNode,e不是单个节点,而是树的头节点
  3. else,e是链表的头节点

前面两种情况都很简单:

if (e.next == null)
    newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

对于第一种单个节点的情况,如源码所示,会用e的hash值与新容量-1的值进行与运算,即将e的hash值映射到newTab的索引上,直接然后newTab[计算出来的新索引] 指向该 Node节点。
对于第二种情况,e不是单个节点,而是树的头节点:

((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

需要调用该节点的split方法,将红黑树切分为两个链表,之后进行扩容操作。
第三种情况,e是链表的头节点,处理部分的源码:

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;
}

首先定义了一些局部变量:

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;

然后用一个do-while循环:

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);

在这里插入图片描述

在这里插入图片描述
第三种情况看似代码多,但比较简单。第三种情况是oldTab[j]位置的元素是一条链表,迁移到新数组上怎么做的?
通过遍历这条链上的节点e,然后根据下面的条件判断:

(e.hash & oldCap) == 0

把这条链上的所有节点分成了两块,e.hash & oldCap结果等于0,e.hash & oldCap结果不等于0。
在这里插入图片描述

// 定义两个链表的头和尾节点,分别用于低位链表和高位链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next; // 用于暂存当前节点的下一个节点

do {
    next = e.next; // 存储当前节点的下一个节点,以便迭代继续进行

    // 判断当前节点的哈希值与oldCap的关系,oldCap通常是数组的当前容量,用作重新哈希的分界线
    if ((e.hash & oldCap) == 0) {
        // 当前节点哈希值和oldCap按位与结果为0,表示当前节点应保持在原索引
        if (loTail == null)
            loHead = e; // 如果低位链表尾节点为空,即链表还未初始化,则当前节点为链表头节点
        else
            loTail.next = e; // 如果低位链表尾节点不为空,则将当前节点链接到链表尾部
        loTail = e; // 更新低位链表尾节点为当前节点
    }
    else {
        // 当前节点哈希值和oldCap按位与结果不为0,表示当前节点应移动到新索引位置
        if (hiTail == null)
            hiHead = e; // 如果高位链表尾节点为空,即链表还未初始化,则当前节点为链表头节点
        else
            hiTail.next = e; // 如果高位链表尾节点不为空,则将当前节点链接到链表尾部
        hiTail = e; // 更新高位链表尾节点为当前节点
    }
} while ((e = next) != null); // 迭代直到链表末尾

// 处理完所有节点后,设置链表尾部节点的next指针为null,表示链表结束
if (loTail != null) {
    loTail.next = null; // 低位链表尾节点next设为null
    newTab[j] = loHead; // 将低位链表头节点放入新表的对应位置
}
if (hiTail != null) {
    hiTail.next = null; // 高位链表尾节点next设为null
    newTab[j + oldCap] = hiHead; // 将高位链表头节点放入新表的偏移位置
}

我们在计算旧表中的节点e,在新表中的索引位置时,是用e.hash & (newCap - 1)计算的,为什么这里不这样计算?
HashMap的扩容过程中,节点在新表中的索引位置通常是通过e.hash & (newCap - 1)来计算的。这种计算方法确保了节点根据其哈希值被均匀分布到新的哈希表中。然而,在你提到的代码片段中,使用的是e.hash & oldCap,这里有一个特定的原因和上下文:

背景
在Java的HashMap中,容量(cap)始终是2的幂次方(例如,16, 32, 64等)。这样设计的主要原因是利用位运算来替代模运算,从而提高计算效率。当cap是2的幂次方时,newCap就是oldCap * 2

为什么使用e.hash & oldCap

  1. 掩码计算: 当使用e.hash & oldCap时,我们实际上是在利用oldCap作为一个掩码。在扩容操作中,oldCap是原哈希表的大小,而newCap是新哈希表的大小,且newCap = 2 * oldCap。在这种情况下,oldCap的值实际上是一个比新容量少一位的值(如,如果oldCap = 16,则oldCap - 1 = 15,二进制为1111)。

  2. 决定节点的分布:

    • 低位桶: 如果e.hash & oldCap的结果是0,这意味着在oldCap位上e.hash是0,因此e.hash的二进制表示中,从最低位到oldCap位的部分没有变化。这表明,节点在新表中的索引位置与在旧表中的位置相同,即index
    • 高位桶: 如果结果不是0,意味着在oldCap的位置e.hash是1,这表明节点在新表中的索引位置是它在旧表中的位置加上oldCap,即index + oldCap

总结
通过这种方式,split操作不需要对每个节点重新计算其在新表中的完整位置,而只需要决定它是留在原位置还是移动到原位置加上旧容量的位置。这简化了计算过程,并利用了已有的哈希值和位运算的性质,达到既快速又高效的重新分配节点的目的。

这种方法不仅保证了正确性,而且也使得扩容过程中的节点重新分配更为高效。

下面用一个具体的例子进行解释:

我们来通过一个具体的例子来解释这个过程。假设我们有一个HashMap,它的原始容量(oldCap)是16,这意味着它可以有从0到15的索引。现在这个HashMap需要扩容,新的容量(newCap)将会是32(即16的两倍)。

哈希表的容量和二进制表示

  • 旧容量 (oldCap): 16,二进制表示为 0001 0000
  • 新容量 (newCap): 32,二进制表示为 0010 0000

索引位置的决定
假设我们有一个键的哈希值为34,我们想确定它在旧表和新表中的位置。

  • 哈希值: 34,二进制表示为 0010 0010

1. 旧表中的位置
在旧表中,索引是通过hash & (oldCap - 1)得到的:

  • oldCap - 1 = 15,二进制表示为 0000 1111
  • 34 & 15 = 0010 0010 & 0000 1111 = 0000 0010 = 2
  • 因此,哈希值为34的键在旧表中的索引是2。

2. 新表中的位置
我们可以通过hash & (newCap - 1)计算新索引,但现在我们关心的是如何使用hash & oldCap来决定索引位置变化:

  • oldCap本身为16,二进制表示为0001 0000
  • 34 & 16 = 0010 0010 & 0001 0000 = 0000 0000
    • 因为结果是0,表示在oldCap位置上的位是0,这意味着哈希值为34的键在新表中的索引仍然是2(即保留在原位置,低位桶)。

如果哈希值是例如50,我们来看看它的处理:

  • 哈希值: 50,二进制表示为 0011 0010
  • 50 & 16 = 0011 0010 & 0001 0000 = 0001 0000
    • 因为结果不是0,表示在oldCap位置上的位是1,这意味着哈希值为50的键在新表中的索引是它原来的索引(旧表中的索引为2)加上oldCap(16),也就是18(即移动到新位置,高位桶)。

总结
这种通过hash & oldCap来判断元素应该留在原位置还是移动到新位置的方法是高效的,因为它利用了已经计算的哈希值,并且只涉及简单的位运算。这样可以在扩容时快速重新分配元素,而无需重新计算每个元素的哈希值。
在这里插入图片描述
最后这个split函数的源码如下:

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 order
    TreeNode<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) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.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);
        }
    }
}

逻辑上和链条拆分类似,不过涉及到树的退化,有时间我在继续写吧。。。。

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

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

相关文章

Shiro反序列化漏洞-Shiro550流程分析

Apache Shiro是一个开源框架&#xff0c;这个漏洞在2016就被披露了。Shiro框架使用广泛&#xff0c;漏洞影响范围广。 环境搭建 这里我使用的是IDEA 2023.3.5 环境下载 这里就不配图片了&#xff0c;具体操作可以搜索引擎 tomcat 8.5.76 下载地址&#xff1a; https://arc…

什么是抽样调查

抽样调查是政府统计工作和市场调查中普遍采用的方法&#xff0c;我国《统计法》中明确规定&#xff1a;搜集、整理统计资料&#xff0c;应当以周期性普查为基础&#xff0c;以经常性抽样调查为主体&#xff0c;综合运用全面调查、重点调查等方法&#xff0c;并充分利用行政记录…

ECMAScript 2024 新特性

ECMAScript 2024 新特性 ECMAScript 2024, the 15th edition, added facilities for resizing and transferring ArrayBuffers and SharedArrayBuffers; added a new RegExp /v flag for creating RegExps with more advanced features for working with sets of strings; and …

Peter算法小课堂—序列切割

讲序列切割之前&#xff0c;先来个铺垫 高手集训 题目描述&#xff1a; 课程表里有连续的n天可以供你选择&#xff0c;每天都有专题课程。其中第i天的专题趣味程度为h[i]。假设你选择了其中连续的若干天&#xff0c;从第l天到第r天。那么&#xff0c; 训练效果 h[l]*1 h[…

WS2812B是一个集控制电路与发光电路于一体的智能外控LED光源

产品概述 WS2812B是一个集控制电路与发光电路于一体的智能外控LED光源。其外型与一个5050LED灯珠相同&#xff0c;每个元件即为一个像素点。像素点内部包含了智能数字接口数据锁存信号整形放大驱动电路&#xff0c;还包含有高精度的内部振荡器和可编程定电流控制部分&a…

神经网络复习--数学相关基础知识

文章目录 矩阵范数卷积激活函数信息熵 矩阵 标量&#xff1a;一个标量就是一个单独的数向量&#xff1a;一个向量是一列数。这些数是有序排列的。通过次序中的索引&#xff0c;我们可以确定每个单独的数矩阵&#xff1a;矩阵是一个二维数组&#xff0c;其中的每个元素被两个索…

2023年上半年信息系统项目管理师——综合知识真题与答案解释(4)

2023年上半年信息系统项目管理师 ——综合知识真题与答案解释(4) 61、文档的规范化管理主要体现在&#xff08;&#xff09;方面。 ①文档书写规范 ②文档质量级别 ③图表编号规则 ④文档目录编写标准 ⑤文档管理制度 ⑥文档安全标准 A&#xff0e;①②③④ B&#xff0e;②③…

STM32使用ESP01S连接阿里云物联网平台

一、ESP01S烧录MQTT固件准备 首先准备好烧录工具&#xff0c;可以从官网上进行下载。 MQTT固件官网网址&#xff1a;AT固件汇总 | 安信可科技 (ai-thinker.com) 进去后如下图界面&#xff0c;向下翻找找到MQTT固件&#xff08;1471&#xff09;下载固件即可。 烧录工具光网地…

信号和槽基本概念

&#x1f40c;博主主页&#xff1a;&#x1f40c;​倔强的大蜗牛&#x1f40c;​ &#x1f4da;专栏分类&#xff1a;QT❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、概述 二、信号的本质 三、槽的本质 一、概述 在 Qt 中&#xff0c;用户和控件的每次交互过程称…

服务器端口怎么开,服务器端口的开放通常涉及的五个关键步骤

服务器端口的开放通常涉及五个关键步骤&#xff0c;包括修改防火墙规则、配置服务器软件以及验证端口是否开放。下面将详细介绍每个步骤。 一、您需要确定需要开放的端口。常见的服务器应用程序端口包括HTTP&#xff08;80端口&#xff09;、HTTPS&#xff08;443端口&#xff…

掌握这个Jenkins插件,离测试开发又近一步!

Jenkins Pipeline是一种可编程的、可扩展的持续交付管道&#xff0c;允许您使用脚本来定义整个软件交付过程。 以下是使用Jenkins Pipeline创建和配置流水线的基本步骤。 Part 01. 创建一个Pipeline Job 在Jenkins中创建一个新的"Pipeline"类型的Job。 以下是在J…

李开复引领的零一万物开源了Yi-1.5模型,推出了6B、9B、34B三个不同规模的版本

零一万物&#xff0c;由李开复博士引领的AI 2.0公司&#xff0c;近期开源了其备受瞩目的Yi-1.5模型&#xff0c;这一举措再次彰显了公司在人工智能领域的创新实力与开放精神。Yi-1.5模型作为零一万物的重要技术成果&#xff0c;不仅代表了公司在大模型技术研发上的新高度&#…

下载npm I就包错解决方案

npm i xxxx -S --legacy-peer-deps 如果包错就执行以上命令

聚类分析 | 基于GA遗传算法优化kmeans聚类(Matlab)

聚类分析 | 基于GA遗传算法优化kmeans聚类&#xff08;Matlab&#xff09; 目录 聚类分析 | 基于GA遗传算法优化kmeans聚类&#xff08;Matlab&#xff09;效果一览基本介绍程序设计参考资料 效果一览 基本介绍 GA-kmeans聚类算法&#xff0c;通过GA遗传算法优化kmeans聚类&…

权力集中,效率提升,中心化模式的优势与挑战

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 &#x1f680; 转载自热榜文章&#x1f525;&#xff1a;探索设计模式的魅力&#xff1a;权力集中…

电子杂志制作攻略,轻松打造高质量数字出版物

随着数字科技的飞速发展&#xff0c;电子杂志作为一种新型的数字出版物&#xff0c;已经越来越受到人们的青睐。它不仅具有丰富的内容、多样的形式&#xff0c;还具有便捷的传播和阅读方式。如今&#xff0c;电子杂志已经逐渐成为企业、媒体和个人展示自身品牌、传播信息的重要…

AI 图像生成-环境配置

一、python环境安装 Windows安装Python&#xff08;图解&#xff09; 二、CUDA安装 CUDA安装教程&#xff08;超详细&#xff09;-CSDN博客 三、Git安装 git安装教程&#xff08;详细版本&#xff09;-CSDN博客 四、启动器安装 这里安装的是秋叶aaaki的安装包 【AI绘画…

自媒体从0-1起号全流程落地指南。(含工具)

下面开始进入主题&#xff1a; 一、持续涨粉的技巧 持续账号的账号通常是具备以下的几种特征 ①利他性&#xff1a;利他性的核心在于你向用户提供了什么&#xff1f; 可以透过逆向思维来体现&#xff0c;首先要明确目标人群及其需求&#xff0c;然后根据这些需求提供必要的…

日本站群服务器的优点以及适合该服务器的业务类型?

日本站群服务器的优点以及适合该服务器的业务类型? 日本站群服务器是指位于日本地区的多个网站共享同一台服务器的架构。这种服务器架构有着诸多优点&#xff0c;使其成为许多企业和网站管理员的首选。以下是日本站群服务器的优点以及适合该服务器的业务类型的分析&#xff1…

Day26 代码随想录打卡|栈与队列篇---有效的括号

题目&#xff08;leecode T20&#xff09;&#xff1a; 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以…