JDK1.8 --- HashMap源码解析

本文深入探讨了HashMap的工作原理,包括其内部结构、初始化过程、扩容机制、哈希算法、冲突处理及红黑树优化等内容。

HashMap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 HashTable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。


HashMap源码

  • 成员变量
 /**
  * 默认的初试容量 16 , 容量必须是2的幂次方
  * 
  * The default initial capacity - MUST be a power of two.
  */
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

 /**
  * 最大容量
  * 
  * The maximum capacity, used if a higher value is implicitly specified
  * by either of the constructors with arguments.
  * MUST be a power of two <= 1<<30.
  */
  static final int MAXIMUM_CAPACITY = 1 << 30;

 /**
  * 默认负载因子 0.75,存储容量达到总容量的0.75被开始扩容
  * 
  * The load factor used when none specified in constructor.
  */
  static final float DEFAULT_LOAD_FACTOR = 0.75f;

 /**
  * threshold表示当HashMap的size大于threshold时会执行resize操作。 
  * threshold=capacity*loadFactor
  * 一个桶的树化阈值 当桶中元素个数超过这个值时 需要使用红黑树节点替换链表节点
  * 
  * The bin count threshold for using a tree rather than list for a
  * bin.  Bins are converted to trees when adding an element to a
  * bin with at least this many nodes. The value must be greater
  * than 2 and should be at least 8 to mesh with assumptions in
  * tree removal about conversion back to plain bins upon
  * shrinkage.
  */
  static final int TREEIFY_THRESHOLD = 8;

 /**
  * 一个树的链表还原阈值 当扩容时,桶中元素个数小于这个值 
  * 就会把树形的桶元素 还原(切分)为链表结构
  * 
  * The bin count threshold for untreeifying a (split) bin during a
  * resize operation. Should be less than TREEIFY_THRESHOLD, and at
  * most 6 to mesh with shrinkage detection under removal.
  */
  static final int UNTREEIFY_THRESHOLD = 6;

 /**
  * 哈希表的最小树形化容量
  * 当哈希表中的容量大于这个值时,表中的桶才能进行树形化
  * 否则桶内元素太多时会扩容,而不是树形化
  * 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
  * 
  * The smallest table capacity for which bins may be treeified.
  * (Otherwise the table is resized if too many nodes in a bin.)
  * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
  * between resizing and treeification thresholds.
  */
  static final int MIN_TREEIFY_CAPACITY = 64;
.
.
.
 /**
  * 哈希表-存放元素,它的大小总是2的幂次方
  * The table, initialized on first use, and resized as
  * necessary. When allocated, length is always a power of two.
  * (We also tolerate length zero in some operations to allow
  * bootstrapping mechanics that are currently not needed.)
  */
 transient Node<K,V>[] table;

 /**
  * 缓存记录集
  * Holds cached entrySet(). Note that AbstractMap fields are used
  * for keySet() and values().
  */
 transient Set<Map.Entry<K,V>> entrySet;

 /**
  * 大小
  * The number of key-value mappings contained in this map.
  */
 transient int size;

 /**
  * fast-fail机制-记录hashMap被修改的次数
  * The number of times this HashMap has been structurally modified
  * Structural modifications are those that change the number of mappings in
  * the HashMap or otherwise modify its internal structure (e.g.,
  * rehash).  This field is used to make iterators on Collection-views of
  * the HashMap fail-fast.  (See ConcurrentModificationException).
  */
 transient int modCount;

 /**
  * 扩容阀值 capacity * load factor
  * The next size value at which to resize (capacity * load factor).
  *
  * @serial
  */
 // (The javadoc description is true upon serialization.
 // Additionally, if the table array has not been allocated, this
 // field holds the initial array capacity, or zero signifying
 // DEFAULT_INITIAL_CAPACITY.)
 int threshold;

 /**
  * 真实的负载因子
  * The load factor for the hash table.
  *
  * @serial
  */
 final float loadFactor;

成员方法

  • 构造方法
 /**
  * 无参的构造方法(初试容量16,负载因子0.75)
  * Constructs an empty <tt>HashMap</tt> with the default initial capacity
  * (16) and the default load factor (0.75).
  */
 public HashMap() {
     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
 }
  /**
 - 有参数的构造方法
 - Constructs an empty <tt>HashMap</tt> with the specified initial
 - capacity and load factor.
 -  5. @param  initialCapacity the initial capacity
 - @param  loadFactor      the load factor
 - @throws IllegalArgumentException if the initial capacity is negative
 -         or the load factor is nonpositive
   */
  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 = (需要存储的元素(键值对)个数 / 负载因子) + 1。
//HashMap并没有真实容量这个成员属性,但可以通过上面的方式得出
-->HashMap(int initialCapacity, float loadFactor)
  -->this.loadFactor = loadFactor;
  -->this.threshold = tableSizeFor(initialCapacity)
/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
	int n = cap - 1;
	 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;
}

HashMap初始化的过程其实就是负载因子,和扩容阀值的设置过程。
其中tableSizeFor方法很有意思:
我们现在假设入参为9,我们先看看这一系列操作到底干了些什么。

int n = cap -1 = 9 -1 = 8 [1000]
n |= n >>> 1;
>>> :代表无符号右移动,也就是向右移位,高位直接补0  n |= 1 等同于 n = n | 1
n |= n >>> 1 = [[1000] >>> 1] | [1000] = [0100] | [1000] = [1100] 
n |= n >>> 2 = [[1100] >>> 2] | [1100] = [0011] | [1100] = [1111]
n |= n >>> 4 = [[1010] >>> 4] | [1111] = [0000] | [1111] = [1111]
...
n = 1111 = 16
return n+1 = 16
由此可见,这一系列操作会使得目标二进制数转化成大于当前数的最小二次方幂 - 1
例如 0001 --> 0001[2-1]       0010 --> 0011[4-1] 
     0100 --> 0111[8-1]       1000 --> 1111[16-1]
最后将结果加一就得到了大于当前数的最小的二次方幂,这个方法非常重要,这个是保证了hashMap的容量始终为2的幂次方的。

tableSizeFor
从源码注释可以看出:java规定HashMap的容量DEFAULT_INITIAL_CAPACITY必须为2的幂次方。那么为什么这么设计呢?下面通过put方法来为大家解答。

  • put方法
-->put(K key, V value)
  -->putVal(hash(key), key, value, false, true)
    -->if ((tab = table) == null || (n = tab.length) == 0)
      -->n = (tab = resize()).length; //扩容
    //将hash值映射到指定到tab索引位
    //一.映射到的索引位为空,直接put  
    -->if((p = tab[i = (n - 1) & hash]) == null)
      -->tab[i] = newNode(hash, key, value, null);
    //2.//映射到的索引位不为空(说明出现hash冲突),往桶内(链表或者树)插入   
    -->else  
      //1.如果key是相同的(equals 或者 ==),key的hash值也是相同的
      -->(p.hash == hash &&
         ((k = p.key) == key || (key != null && key.equals(k))))
        -->if (e != null) && (!onlyIfAbsent || oldValue == null)
          -->e.value = value//覆盖,默认覆盖 
      //2.key值不同,只是hash冲突,桶内是树结构
	  -->if (p instanceof TreeNode) 桶内是树结构
	    //往树内插入
	    -->e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)
	  //3.key值不同,只是hash冲突,桶内是链表结构  
	  -->else //桶内是链表结构
	    //在遍历的过程中每次都需要判断key值是否冲突,冲突直接覆盖(代码见源码)
	    //插入到链表最后
	    -->p.next = newNode(hash, key, value, null); 
	    -->if (binCount >= TREEIFY_THRESHOLD - 1) //如果长度到达桶的树化阀值
	      -->treeifyBin(tab, hash); //将链表转化成树

       接下来我们来分析:容量为什么一定要是2的幂次方,其实,从put方法中定位插入索引位置的时候用了(n - 1) & hash,这里的 n 就是当前Map的容量,这个操作是因为Hash本身太大,不可能直接映射到一个索引,通过这个方式我们可以保证每一个待插入的值都能映射到hash表中,我们现假设容量为 n = 16 ,那么 n-1 = 15 [1111]。(n - 1) & hash 的结果实际上就是截取来hash值的低四位,(2的幂次方 -1 )的低位一定全是 1。那如果容量 n = 9 那么 n-1 = [1000] ,(n - 1) & hash 的结果仅仅只保留了倒数第四位,那么插入的值就只能放在 [0000,1000] , 那么hash冲突的概率将会极高。
       所以,使用2的幂次方作为容量,是为了降低hash冲突考虑。

  • hash方法
 /**
  * 扰动函数
  * Computes key.hashCode() and spreads (XORs) higher bits of hash
  * to lower.  Because the table uses power-of-two masking, sets of
  * hashes that vary only in bits above the current mask will
  * always collide. (Among known examples are sets of Float keys
  * holding consecutive whole numbers in small tables.)  So we
  * apply a transform that spreads the impact of higher bits
  * downward. There is a tradeoff between speed, utility, and
  * quality of bit-spreading. Because many common sets of hashes
  * are already reasonably distributed (so don't benefit from
  * spreading), and because we use trees to handle large sets of
  * collisions in bins, we just XOR some shifted bits in the
  * cheapest possible way to reduce systematic lossage, as well as
  * to incorporate impact of the highest bits that would otherwise
  * never be used in index calculations because of table bounds.
  */
 static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }

       在之前的put方法之前,我们需要计算key的hash值,用以确定插入的值到底映射到哪一个位置上,这里并没有使用对象自己的hash方法求hash值,是因为直接将系统给出的hash值,直接取低四位做映射冲突依旧很大,所以这里jdk提供给我们一个hash方法(对hash值做扰动)。
一. key == null ,它的hash值直接给0
二. (h = key.hashCode()) ^ (h >>> 16)
这个操作实际上首先将 hash值 的高十六位移到低十六位去,然后与hash值做按位异或操作,那么这样一来等同于低位保存高位的信息,使得冲突大大降低。这就是扰动函数的作用。
扰动函数

  • resize方法
    当插入元素后到达扩容阀值,开始扩容。
newThr = oldThr << 1; // double threshold

扩容的新数组容量为原数组的两倍

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) {
        // 原数组长度大于最大容量(1073741824) 则将threshold设为Integer.MAX_VALUE=2147483647
        // 接近MAXIMUM_CAPACITY的两倍
        threshold = Integer.MAX_VALUE;
        return oldTab;
    } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
        // 新数组长度 是原来的2倍,
        // 临界值也扩大为原来2倍
        newThr = oldThr << 1;
    }
} else if (oldThr > 0) {
    // 如果原来的thredshold大于0则将容量设为原来的thredshold
    // 在第一次带参数初始化时候会有这种情况
    newCap = oldThr;
} else {
    // 在默认无参数初始化会有这种情况
    newCap = DEFAULT_INITIAL_CAPACITY;// 16
    newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);// 0.75*16=12
}
if (newThr == 0) {
    // 如果新 的容量 ==0
    float ft = (float) newCap * loadFactor;// loadFactor 哈希加载因子 默认0.75,可在初始化时传入,16*0.75=12 可以放12个键值对
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
}
threshold = newThr;// 将临界值设置为新临界值
if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
        Node<K,V> e;
        if ((e = oldTab[j]) != null) {
            oldTab[j] = null;
            //1:指定的桶内只有一个元素,直接重新分布
            if (e.next == null)
                newTab[e.hash & (newCap - 1)] = e;
            //2:指定的桶内是一颗红黑树(红黑树的扩容和链表类似)    
            else if (e instanceof TreeNode)
                ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
            //3:指定的桶内是一个链表    
            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;
                    //假设oldCap=16,hash值的倒数第五位为0,该键值对位置不做改变
                    //将所有值为0的链表连起来
                    if ((e.hash & oldCap) == 0) {
                        if (loTail == null)
                            loHead = e;
                        else
                            loTail.next = e;
                        loTail = e;
                    }
                    //假设oldCap=16,hash值的倒数第五位为1,该键值对位置[原位置+oldCap]
                    ////将所有值为1的链表连起来
                    else {
                        if (hiTail == null)
                            hiHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e;
                    }
                } while ((e = next) != null); 
                if (loTail != null) {
                    loTail.next = null;
                    //放在原位置 newTab[j] 
                    newTab[j] = loHead;
                }
                if (hiTail != null) {
                    hiTail.next = null;
                    //放在新位置 newTab[j + oldCap]
                    newTab[j + oldCap] = hiHead;
                }
            }
        }
    }
}

以下是扩容简要的过程:
HashMap扩容
总结:
次扩充为:与负载因子无关;第二次及以后为:与负载因子有关。

如果loadFactor很小很小,那么map中的table需要不断的扩容,导致除数越来越大,冲突越来越小!

如果loadFactor很大很大,那么当map中table放满了也不要求扩容,导致冲突越来越多,解决冲突而起的链表越来越长!


TreeNode解析

/**
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        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;

       以上是TreeNode的组成结构,它继承自Entry,Entry又继承自Node。所以这个时候节点既有树的特性又有链表的特性。

我们重点来看一下它的put方法:(这个方法特别重要,是后面理解split方法的基础)

/**
 * 当存在hash碰撞的时候,且元素数量大于8个时候,就会以红黑树的方式将这些元素组织起来
 * map 当前节点所在的HashMap对象
 * tab 当前HashMap对象的元素数组
 * h   指定key的hash值
 * k   指定key
 * v   指定key上要写入的值
 * 返回:指定key所匹配到的节点对象,针对这个对象去修改V(返回空说明创建了一个新节点)
 */
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                int h, K k, V v) {
    Class<?> kc = null; // 定义k的Class对象
    boolean searched = false; // 标识是否已经遍历过一次树,未必是从根节点遍历的,但是遍历路径上一定已经包含了后续需要比对的所有节点。
    TreeNode<K,V> root = (parent != null) ? root() : this; // 父节点不为空那么查找根节点,为空那么自身就是根节点
    for (TreeNode<K,V> p = root;;) { // 从根节点开始遍历,没有终止条件,只能从内部退出
        int dir, ph; K pk; // 声明方向、当前节点hash值、当前节点的键对象
        if ((ph = p.hash) > h) // 如果当前节点hash 大于 指定key的hash值
            dir = -1; // 要添加的元素应该放置在当前节点的左侧
        else if (ph < h) // 如果当前节点hash 小于 指定key的hash值
            dir = 1; // 要添加的元素应该放置在当前节点的右侧
        else if ((pk = p.key) == k || (k != null && k.equals(pk))) // 如果当前节点的键对象 和 指定key对象相同
            return p; // 那么就返回当前节点对象,在外层方法会对v进行写入
 
        // 走到这一步说明 当前节点的hash值  和 指定key的hash值  是相等的,但是equals不等
        else if ((kc == null &&
                    (kc = comparableClassFor(k)) == null) ||
                    (dir = compareComparables(kc, k, pk)) == 0) {
 
            // 走到这里说明:指定key没有实现comparable接口   或者   实现了comparable接口并且和当前节点的键对象比较之后相等(仅限第一次循环)
        
 
            /*
             * searched 标识是否已经对比过当前节点的左右子节点了
             * 如果还没有遍历过,那么就递归遍历对比,看是否能够得到那个键对象equals相等的的节点
             * 如果得到了键的equals相等的的节点就返回
             * 如果还是没有键的equals相等的节点,那说明应该创建一个新节点了
             */
            if (!searched) { // 如果还没有比对过当前节点的所有子节点
                TreeNode<K,V> q, ch; // 定义要返回的节点、和子节点
                searched = true; // 标识已经遍历过一次了
                /*
                 * 红黑树也是二叉树,所以只要沿着左右两侧遍历寻找就可以了
                 * 这是个短路运算,如果先从左侧就已经找到了,右侧就不需要遍历了
                 * find 方法内部还会有递归调用。参见:find方法解析
                 */
                if (((ch = p.left) != null &&
                        (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                        (q = ch.find(h, k, kc)) != null))
                    return q; // 找到了指定key键对应的
            }
 
            // 走到这里就说明,遍历了所有子节点也没有找到和当前键equals相等的节点
            dir = tieBreakOrder(k, pk); // 再比较一下当前节点键和指定key键的大小
        }
 
        TreeNode<K,V> xp = p; // 定义xp指向当前节点
        /*
        * 如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就可以把要添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较
        * 如果dir大于等于0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素作为当前节点的右节点,如果不为空,还需要下一轮继续比较
        * 如果以上两条当中有一个子节点不为空,这个if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较
        */
        if ((p = (dir <= 0) ? p.left : p.right) == null) {  
            // 如果恰好要添加的方向上的子节点为空,此时节点p已经指向了这个空的子节点
            Node<K,V> xpn = xp.next; // 获取当前节点的next节点
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); // 创建一个新的树节点
            if (dir <= 0)
                xp.left = x;  // 左孩子指向到这个新的树节点
            else
                xp.right = x; // 右孩子指向到这个新的树节点
            xp.next = x; // 链表中的next节点指向到这个新的树节点
            x.parent = x.prev = xp; // 这个新的树节点的父节点、前节点均设置为 当前的树节点
            if (xpn != null) // 如果原来的next节点不为空
                ((TreeNode<K,V>)xpn).prev = x; // 那么原来的next节点的前节点指向到新的树节点
            moveRootToFront(tab, balanceInsertion(root, x));// 重新平衡,以及新的根节点置顶
            return null; // 返回空,意味着产生了一个新节点
        }
    }
}
find方法解析
                 */
                if (((ch = p.left) != null &&
                        (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                        (q = ch.find(h, k, kc)) != null))
                    return q; // 找到了指定key键对应的
            }

            // 走到这里就说明,遍历了所有子节点也没有找到和当前键equals相等的节点
            dir = tieBreakOrder(k, pk); // 再比较一下当前节点键和指定key键的大小
        }

        TreeNode<K,V> xp = p; // 定义xp指向当前节点
        /*
        * 如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就可以把要添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较
        * 如果dir大于等于0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素作为当前节点的右节点,如果不为空,还需要下一轮继续比较
        * 如果以上两条当中有一个子节点不为空,这个if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较
        */
        if ((p = (dir <= 0) ? p.left : p.right) == null) {  
            // 如果恰好要添加的方向上的子节点为空,此时节点p已经指向了这个空的子节点
            Node<K,V> xpn = xp.next; // 获取当前节点的next节点
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); // 创建一个新的树节点(此时x.next =xpn)
            if (dir <= 0)
                xp.left = x;  // 左孩子指向到这个新的树节点
            else
                xp.right = x; // 右孩子指向到这个新的树节点
            xp.next = x; // 链表中的next节点指向到这个新的树节点
            x.parent = x.prev = xp; // 这个新的树节点的父节点、前节点均设置为 当前的树节点
            if (xpn != null) // 如果原来的next节点不为空
                ((TreeNode<K,V>)xpn).prev = x; // 那么原来的next节点的前节点指向到新的树节点
            moveRootToFront(tab, balanceInsertion(root, x));// 重新平衡,以及新的根节点置顶
            return null; // 返回空,意味着产生了一个新节点
        }
    }
}

我们前面有提到TreeNode其实也是一个二叉树,那么它是怎么构建成一个二叉树的呢?
请看下图:
TreeNode插入

  • split
           这个方法是前面扩容的时候有涉及的,这里我们来看一看,上面说过这里和链表的极其相似,因为红黑树我们事先维护来一个链表,所以对于树的处理就基本和链表的处理相似了,唯一不同的是,当元素个数到达阀值的时候会涉及树和链表的转变,请看源码。
/**
 * Splits nodes in a tree bin into lower and upper tree bins,
 * or untreeifies if now too small. Called only from resize;
 * see above discussion about split bits and indices.
 *  * @param map the map
 * @param tab the table for recording bin heads
 * @param index the index of the table being split
 * @param bit the bit of hash to split on
  */
 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);
         }
     }
 }
  • treeifyBin
    树化
          HashMap 在设计之初,并没有考虑到以后会引入红黑树进行优化。所以并没有像 TreeMap 那样,要求键类实现 comparable 接口或提供相应的比较器。但由于树化过程需要比较两个键对象的大小,在键类没有实现 comparable 接口的情况下,怎么比较键与键之间的大小呢?
          上面的插入其实已经涉及了,无非就是三个步骤:
          1:比较键与键之间 hash 的大小,如果 hash 相同,继续往下比较。
          2:检测键类是否实现了 Comparable 接口,如果实现调用 compareTo 方法进行比较。
          3:如果仍未比较出大小,就需要进行仲裁了,仲裁方法为 tieBreakOrder , 这个方法比较简单粗暴直接比较hashCode大小。
    树化的过程比较简单,真正需要了解的是红黑树。(网上有很多红黑树教程,可以自行去看)
    在这里插入图片描述
内容概要:本文聚焦于分布式传感器网络中的LEACH(Low-Energy Adaptive Clustering Hierarchy)聚类算法,系统研究其在能量消耗建模与网络生命周期优化方面的性能表现,并结合Matlab代码实现完整的仿真分析流程。研究深入剖析LEACH协议的核心机制,即通过周期性选举簇头节点实现能量负载的均衡分布,从而有效延长网络整体生存时间。内容涵盖传感器节点部署优化、通信能耗模型构建、路由策略设计及能量耗尽过程的动态模拟,重点解决传统LEACH算法中存在的簇头分布不均、能耗集中于特定区域等缺陷。文档不仅提供了LEACH及其改进算法的仿真案例,还拓展至智能优化算法、机器学习、信号处理等多学科交叉应用方向,体现了该研究在物联网、边缘计算和无线传感网络领域的广泛适用性与科研价值。; 适合人群:具备一定编程基础和科研能力,熟悉Matlab仿真环境,从事无线传感器网络、物联网、智能优化算法等相关领域的研究生或科研人员。; 使用场景及目标:①用于无线传感器网络中能量高效路由协议的设计与优化;②通过Matlab仿真实现LEACH算法及其改进版本的性能对比分析;③支撑科研论文复现、算法验证与教学演示;④为分布式系统中的能耗均衡问题提供解决方案参考。; 阅读建议:建议读者按照文档提供的目录结构系统学习,重点关注LEACH算法的核心机制与能量模型构建,结合所提供的Matlab代码进行仿真实践,并参考网盘资源中的完整案例以加深理解。同时可拓展至其他优化算法与通信协议的研究,提升综合科研能力。
内容概要:本文针对电力系统中考虑N-1故障集的安全约束经济调度(SCED)问题,提出了一种兼顾系统安全性与经济性的优化建模方法,并提供了基于Matlab的代码实现。N-1故障集指系统中任一关键元件(如输电线路或发电机)发生故障退出运行的情形,确保在此类故障下系统仍能安全稳定运行是调度决策的核心要求。所构建的SCED模型在满足功率平衡、机组出力能力和线路传输容量等基本物理约束的基础上,进一步引入N-1故障后的安全校验约束,通过优化算法求解出一组既能维持系统安全稳定又可实现发电成本最小化的机组调度方案。该研究对于提升电网韧性、保障供电可靠性以及支撑现代电力系统的安全经济运行具有重要的理论价值与实践意义。; 适合人群:具备电力系统分析、运筹优化理论基础及Matlab编程能力的高校研究生、科研人员和电力行业相关技术人员。; 使用场景及目标:①深入理解安全约束经济调度(SCED)的基本原理与数学建模流程;②掌握N-1安全准则在优化模型中的具体建模方法与实现逻辑;③获取可复现、可调试的Matlab代码实例,用于教学示范、科研复现或作为进一步开发复杂调度模型的基础。; 阅读建议:建议读者结合文档内容与配套代码,重点关注模型中关于N-1故障场景的处理机制与约束构建方式,通过逐步调试与仿真分析,深化对目标函数、决策变量与多重安全约束之间耦合关系的理解。
代码下载链接: https://pan.quark.cn/s/a4b39357ea24 在信息技术领域中,人工智能(AI)已经演变成一个至关重要的分支,特别是以深度学习和神经网络为代表的技术正持续促进科技的发展。本文将聚焦于“人工智能学习路线图”这一核心议题,详细剖析相关知识点,以协助学习者构建一个系统化的知识体系。 从标题入手,“人工神经网络_1、深度学习_1、数学基础_1、深度学习之外的人工智能_1”,这四个关键部分构成了人工智能学习的核心框架。 1. **人工神经网络**:作为人工智能领域的基础,该技术通过模拟人脑神经元的工作机制来构建模型。神经网络包含输入层、隐藏层和输出层,借助权重的调节来处理信息,从而达成分类、识别或预测等任务。掌握神经网络的构造、反向传播方法、激活函数(例如sigmoid、ReLU)以及损失函数(比如均方误差、交叉熵)是学习该领域的基础。 2. **深度学习**:深度学习属于机器学习的一个子集,它借助多层神经网络来识别复杂的模式。深度学习的优势在于能够处理高维数据,例如图像、声音和文本。卷积神经网络(CNN)在图像识别领域效果显著,而循环神经网络(RNN)和长短期记忆网络(LSTM)则适合处理序列数据。除此之外,生成对抗网络(GAN)等生成模型技术也值得关注。 3. **数学基础**:深度学习和神经网络的理论支撑依赖于数学。线性代数通过矩阵运算和特征分解,为理解神经网络的优化过程提供了支持;微积分和梯度下降构成了优化算法的基础,特别是在反向传播中的参数调整;概率论与统计学是理解和构建模型的关键,如贝叶斯定理和最大似然估计;此外,还涉及到优化理论(比如牛顿法、拟牛顿法)和凸分析。 4. **深度学习之外的人工智能**:人工智能的应用范...
源码直接下载地址: https://pan.quark.cn/s/eda4370d97f3 溪谷H5游戏平台联运系统V3.0是一款为H5游戏运营人员量身定制的高效、稳固且具备多种功能的管理解决方案。联运平台是网络游戏领域中常见的商业模式,它允许多个合作方共同推广同一款游戏,并通过利润分配的方式共同享有收益。借助这一系统,开发团队与运营商能够方便地处理游戏、用户、渠道、财务等多个核心领域,达成迅速发布和高效执行的目标。 1. **系统架构** - **前端框架**:该系统或许运用了如React或Vue.js等现代前端技术,确保用户能够获得顺畅的操作体验。 - **后端框架**:可能依托于Node.js、PHP或Java等后端技术,负责执行业务逻辑和数据交流。 - **数据库**:一般会采用MySQL或MongoDB等数据库管理系统来保存用户资料、游戏数据及运营数据统计等信息。 2. **核心功能** - **游戏管理**:系统应能够支持H5游戏的上传、发布、更新,并对游戏状态进行监控,包括游戏的发布、下架、版本迭代等操作。 - **渠道管理**:联运平台需要整合多种推广渠道,例如微信、QQ、浏览器等,并对每个渠道的推广成效进行监测和分析。 - **用户管理**:涵盖用户注册、登录、个人信息维护,以及用户行为数据的收集和剖析。 - **财务管理**:提供详尽的收入报告,包括渠道分成、充值记录、提现请求等,有助于运营商进行账目审核。 - **推广活动**:支持创建和管理各类营销活动,如限时优惠、新手礼包、积分兑换等,以提升用户活跃度和付费转化率。 - **统计分析**:提供即时的运营数据统计,例如DAU(日活跃用户)、ARPU(每用户平均收入)、留存率等,助力优化运营策略...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值