HashMap 源码学习-jdk1.8

1、一些常量的定义

这里针对MIN_TREEIFY_CAPACITY 这个值进行解释一下。

java8里面,HashMap 的数据结构是数组 + (链表或者红黑树),每个数组节点下可能会存在链表和红黑树之间的转换,当同一个索引下面的节点超过8个时,首先会看当前数组长度,如果大于64,则会发生链表向红黑树的 转换,否则不会转换,而是扩容。

    // 默认的初始化长度 16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 默认的最大容量 2^30static 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;// map已存节点的数量transient int size;// 修改次数    transient int modCount;// 扩容阈值 当size达到这个值的时候,hashmap开始扩容int threshold;// 加载因子 threshold = 容量 * loadFactorfinal float loadFactor;

2、构造器

HashMap提供了三个构造器。

    // 无参构造器,使用默认加载因子 0.75public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}// 只传入初始化容量,也会使用默认加载因子 0.75public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}// 同时传入初始化容量和加载因子 (初始化容量要大于0,且不能超过最大容量)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;// 初始化的容量先赋值给了threshold 暂存。this.threshold = tableSizeFor(initialCapacity);}

注意看,使用带参构造器 会调用 tableSizeFor(initialCapacity); 这个方法是干嘛的呢?其实就是为了计算初始化容量。HashMap规定,其容量必须是2的N次方

  •  不传初始化容量,就取默认值16
  • 传了初始化容量,则初始化容量设置为大于等于该数值的 一个最小的2的N次方
    • 比如传入了7,不是2的N次方,那么取比他大的最小的2的N次方,就是8
    • 比如传入了8,刚好是2的N次方,那就取8
    • 比如传入了9,不是2的N次方,那么取比他大的最小的2的N次方,就是16
    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;}

或等于操作 a |= b ,其实就是 a = a | b。

无符号右移操作:a >>> b 就表示将a向右移动b位,左边空出来的用0补充,右边的被丢弃

那么 n |= n >>> 1 操作得到的结果就是,最高位和次高位的结果为1;--- > n 的前两位为1

n |= n >>> 2 操作之后 --- > n的前四位为1

.... 一通操作之后,得到的值是一个低位全是1的值。然后返回的时候+1,得到的值就是一个比n大的2的N次方。而开头的 int n = cap - 1 是为了解决本身就是2的N次方的场景。

3、插入操作
3.1、插入操作的具体流程
  1. 插入前首先判断数组是否为空,如果为空就进行初始化
  2. 计算key的hash值,然后和数组长度-1 进行 & 运算,获取在数组中的索引位置
    1. 当前位置不存在元素,就直接创建新节点放在当前索引位置
    2. 当前位置元素存在,就走后续的逻辑
  3. 判断当前坐标下头节点的hash值是否和 key的hash相等,如果相等就进行替换(还要判断一个控参 onlyIfAbsent,这个为false的时候才会替换,最常用的put操作这个值就是false )
  4. 如果不相等,判断当前是链表还是红黑树
    1. 如果是链表,遍历链表节点,并统计节点个数:
      1. 如果找到了相同的key,就进行覆盖操作,
      2. 如果没有找到相同key,就将节点添加到链表最后面,并判断是否超过8个节点,如果大于等于8,就要链表转红黑树操作。
    2. 如果是红黑树:找到红黑树根节点,从根节点开始遍历:
      1. 找到相同的key,就进行替换
      2. 找不到相同的key,就放到相应的位置,然后进行红黑树插入平衡调整
  5. 插入完成之后,判断当前节点数目是否超过扩容阈值,如果超过,就进行扩容。
public V put(K key, V value) {/*** 首先计算出了key的hash 值*/return putVal(hash(key), key, value, false, true);
}final V putVal ( int hash, K key, V value,boolean onlyIfAbsent, boolean evict){HashMap.Node<K, V>[] tab;HashMap.Node<K, V> p;int n, i;/*** 判断数组是否为空,为空则进行数组初始化 * ---> tab = resize() 然后获取数组的长度*/if ((tab = table) == null || (n = tab.length) == 0) {n = (tab = resize()).length;}/*** 计算当前节点要插入数组的索引的位置 ---> (n - 1) & hash* 如果索引处不存在节点,就新创建节点放到索引的位置*/if ((p = tab[i = (n - 1) & hash]) == null) {tab[i] = newNode(hash, key, value, null);}/*** 如果索引处存在节点,走这个逻辑*/else {HashMap.Node<K, V> e;K k;if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {/*** 进入这个分支,说明要插入的节点和头节点的key相同*/e = p;} else if (p instanceof HashMap.TreeNode) {/*** 说明头节点是红黑树了,要把这个新节点插入到红黑树中,涉及到新节点的插入,红黑树的平衡调整等*/e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);} else {/*** 说明头节点是链表节点,遍历链表*/for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {/*** 遍历到最后了,创建新节点插入到尾端* 还要判断节点是否超过8个,超过了要转化为红黑树*/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)))) {/*** 找到了相同key的value*/break;}p = e;}}/*** e不为空,说明有key相同的情况,替换成新的value,然后直接返回旧的节点* 因为节点数目不存在变化,因此不需要进行扩容判断*/if (e != null) { // existing mapping for keyV oldValue = e.value;// onlyIfAbsent的判断if (!onlyIfAbsent || oldValue == null) {e.value = value;}afterNodeAccess(e);return oldValue;}}++modCount;/*** 如果当前节点超过了扩容阈值,就进行扩容,然后返回null*/if (++size > threshold) {resize();}afterNodeInsertion(evict);return null;
}

3.2、 key的hash值是怎么计算的?为什么要这么计算?
  • 如果key为空,就直接返回0
  • 不为空将 key的hashcode 和 hashcode左移16位进行& 运算
  • ---- 左移16位主要就是为了将hash的高位也参与到hash计算中,减少hash冲突。
    static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

3.3、resize扩容流程介绍
  1. 首先会对老数组进行一系列的校验,大致分为:
    1. 老数组为空,就设置一下数组长度和扩容阈值,新建数组,然后返回
    2. 老数组不为空,校验老数组长度,如果长度超过上限,扩容阈值修改为int最大值,返回
    3. 否则:容量、扩容阈值变为原来的2倍
  2. 接着开始遍历老数组
    1. 当前坐标下没有节点,就继续遍历
    2. 当前坐标只有一个节点,计算hash值,然后放到新数组对应位置
    3. 当前坐标是链表,走链表逻辑:
      1. 遍历链表节点,计算  e.hash & oldCap ,这个值如果是0,说明扩容后,在新数组的坐标和老数组一样,如果为1 ,说明扩容后在新数组的坐标应该是 老数组坐标 + 扩容长度,因此通过计算这个值,可以将链表节点分为高位节点和低位节点
      2. 定义高位和低位两个链表,不断将链表节点放在这两个新链表尾端
      3. 然后低位链表放在新数组的i 坐标位置,高位链表放在新数组i+oldcap的位置
    4. 当前坐标是红黑树,走红黑树的逻辑
      1. 因为维护红黑树的时候也维护了一个双向链表,因此通过 e.prev e.next就可以遍历整个树 (也就是说遍历链表就等于遍历树)
      2. 同样是将元素分别放在低位链表和高位链表中,并计算每个链表的长度
      3. 低位链表的头节点放在新数组的i坐标位置,然后维护链表的红黑树结构(维护前会判断高位链表是否有值,如果为空,说明树结构没有被破坏而是直接迁移到新数组中了,这个时候就可以不用重新维护树结构了)
      4. 高位链表头节点放在新数组i+oldcap的位置,维护树结构,同3

注意:jdk1.8中,hashmap的扩容,链表节点处理只遍历了一次,而ConcurrentHashMap中遍历了两次。

final HashMap.Node<K, V>[] resize() {HashMap.Node<K, V>[] oldTab = table;// 临时存储老的数组长度和老的扩容阈值int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;// 定义新的数组长度和新的扩容阈值int newCap, newThr = 0;// oldCap > 0 说明数组已经初始化了if (oldCap > 0) {// 当前数组长度已经大于等于最大数组长度了,就把扩容阈值设置为int最大值返回,不需要扩容了if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 否则,长度变为原来2倍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 defaultsnewCap = 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);}// hashmap 初始化的时候,是将数组初始化长度赋值给了threshold,这里开始才是变成扩容阈值。threshold = newThr;// 创建新的数组,并将新数组赋值给tableHashMap.Node<K, V>[] newTab = (HashMap.Node<K, V>[]) new HashMap.Node[newCap];table = newTab;// 老数组不为空,就走扩容逻辑,否则就直接返回新创建的数组了if (oldTab != null) {// 对老数组开始遍历for (int j = 0; j < oldCap; ++j) {HashMap.Node<K, V> e;// 数组的坐标节点为空说明没数据,直接遍历下个坐标if ((e = oldTab[j]) != null) {oldTab[j] = null;// 只有个节点,直接取出该节点,计算hash值,放到新数组中if (e.next == null) {newTab[e.hash & (newCap - 1)] = e;}// 当前是红黑树,执行红黑树扩容逻辑else if (e instanceof HashMap.TreeNode) {((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);}// 当前是链表,执行链表扩容逻辑else { // preserve order// 定义高位链表和低位链表HashMap.Node<K, V> loHead = null, loTail = null;HashMap.Node<K, V> hiHead = null, hiTail = null;HashMap.Node<K, V> next;// 遍历链表do {next = e.next;// e.hash & oldCap 可以计算出当前节点应该放在高位还是低位if ((e.hash & oldCap) == 0) {// 将遍历到的节点放在loTail尾部// loHead指向低位节点的头节点if (loTail == null) {loHead = e;} else {loTail.next = e;}loTail = e;} else {// 将遍历到的节点放在hiTail尾部// hiHead指向高位节点的头节点if (hiTail == null) {hiHead = e;} else {hiTail.next = e;}hiTail = e;}} while ((e = next) != null);// 低位链表的头节点放在 新数组的原index中if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 高位链表的头节点放在 新数组的原index + oldCap 中if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}final void split(HashMap<K, V> map, HashMap.Node<K, V>[] tab, int index, int bit) {// ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 这个this 就是数组中取出来的第一个元素,也就是树的头节点HashMap.TreeNode<K, V> b = this;// 设置低位首节点和低位尾节点,高位首节点和高位尾节点HashMap.TreeNode<K, V> loHead = null, loTail = null;HashMap.TreeNode<K, V> hiHead = null, hiTail = null;// 这两个值用于记录低位坐标和高位坐标节点的数目int lc = 0, hc = 0;// 从根节点开始,对整个树进行遍历,我们介绍了,红黑树其实也维护了双向链表,因此通过 e.prev  e.next就可以遍历整个树for (HashMap.TreeNode<K, V> e = b, next; e != null; e = next) {next = (HashMap.TreeNode<K, V>) e.next;e.next = null;// bit传入的就是oldCap,也就是旧数组的长度,通过hash & 运算,就可以判断是放在新数组的低位坐标还是高位坐标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) {// 低位节点数目小于等于6,就转为链表if (lc <= UNTREEIFY_THRESHOLD) {tab[index] = loHead.untreeify(map);}// 否则,还是红黑树结构else {// 链表头节点赋值给 tab[index]tab[index] = loHead;if (hiHead != null)// 对低位的链表维护红黑树结构// 为什么加一个hiHead != null 判断呢?因为如果原来的元素全部都分到了低位节点,那说明树结构没有被破坏,就不需要维护了{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);}}}
}

3.4 链表转红黑树

final void treeifyBin(HashMap.Node<K, V>[] tab, int hash) {int n, index;HashMap.Node<K, V> e;// 如果数组为空,或者数组长度小于64,就先尝试扩容,因为链表转树的消耗太大了if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {resize();} // 先拿到当前坐标下的头节点 ,赋值给 eelse if ((e = tab[index = (n - 1) & hash]) != null) {// 定义头节点和尾节点HashMap.TreeNode<K, V> hd = null, tl = null;// 遍历链表do {// 将链表节点转化为红黑树节点HashMap.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);// 截止到目前,把链表中所有的node对象转变为了红黑树节点,单向链表变成了双向链表// 把转换后的双向链表,替换原来位置上的单向链表if ((tab[index] = hd) != null) {// 树化操作hd.treeify(tab);}}
}final void treeify(HashMap.Node<K, V>[] tab) {HashMap.TreeNode<K, V> root = null;// 因为是调用的hd.treeify(tab),因此,这里的this就是双向链表的头节点,这里先赋值给了临时变量x// 开始循环这个双向链表了,x就是循环的元素,next就是下一个节点元素for (HashMap.TreeNode<K, V> x = this, next; x != null; x = next) {next = (HashMap.TreeNode<K, V>) x.next;// 当前节点左右孩子都设置为空x.left = x.right = null;if (root == null) {// 第一次进来,根节点肯定是空,将头节点设置为根节点,染色黑x.parent = null;x.red = false;root = x;} // 第一次以后的循环都走下面的分支了else {// 定义当前节点的key 和 hashK k = x.key;int h = x.hash;Class<?> kc = null;// 开始遍历树结构了for (HashMap.TreeNode<K, V> p = root; ; ) {// ph 和 pk 定义当前树节点的 hash 和 key ,通过hash判断当前节点要放在树的左边还是右边// dir代表 往树左边放还是右边放int dir, ph;K pk = p.key;if ((ph = p.hash) > h) {dir = -1;} else if (ph < h) {dir = 1;} // hash相等的时候,继续一系列的判断,最终得到direlse if ((kc == null && (kc = comparableClassFor(k)) == null)|| (dir = compareComparables(kc, k, pk)) == 0) {dir = tieBreakOrder(k, pk);}HashMap.TreeNode<K, V> xp = p;// dir <= 0 说明是在左侧,否则是在右侧// 只有保证当前树节点没有对应的左孩子或者右孩子的时候,才会将当前节点挂上去,否则继续循环遍历树结构if ((p = (dir <= 0) ? p.left : p.right) == null) {x.parent = xp;if (dir <= 0) {xp.left = x;} else {xp.right = x;}// 红黑树平衡操作root = balanceInsertion(root, x);// 当前节点已经插入红黑树中了,可以跳出当前循环,遍历链表的下一个节点break;}}}}// 把root节点放在当前坐标位置moveRootToFront(tab, root);
}/*** 我们要明确,红黑树节点不但维护了树结构,还维护了双向链表的结构* 这个方法的作用就是:* 1、将树的根节点,赋值给tab[i]* 2、将这个节点,变成双向链表的头节点*/
static <K, V> void moveRootToFront(HashMap.Node<K, V>[] tab, HashMap.TreeNode<K, V> root) {int n;if (root != null && tab != null && (n = tab.length) > 0) {// 通过根节点 hash计算在数组中的索引位置int index = (n - 1) & root.hash;// 取到当前索引的第一个节点HashMap.TreeNode<K, V> first = (HashMap.TreeNode<K, V>) tab[index];// 如果root节点和 当前索引位置第一个节点不一样,就把root节点放在当前坐标位置// 同时要维护双向链表,将root节点变成双向链表的第一个节点。if (root != first) {HashMap.Node<K, V> rn;tab[index] = root;// 将root节点变成双向链表的第一个节点。HashMap.TreeNode<K, V> rp = root.prev;if ((rn = root.next) != null) {((HashMap.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);}
}
4、删除操作
  1. 数组没有初始化,或者对应下标节点为空,说明没有该元素,直接返回null
  2. 查找node,(红黑树或者链表结构)
  3. 删除node,(红黑树或者链表结构) --- 删除节点的时候即便树的元素小于等于6也不会转为链表,在代码里面没看到,只在扩容的时候会有转换操作。
/*** 删除方法,主要介绍以下两个参数* @param matchValue true:只有值也相同的时候才删除* @param movable true:删除后移动节点,树结构的时候会用到,一般为true*/
final HashMap.Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {HashMap.Node<K, V>[] tab;HashMap.Node<K, V> p;int n, index;// 组数没有初始化,或者对应坐标下面没有元素,直接返回null了if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {// node记录要删除的元素HashMap.Node<K, V> node = null, e;K k;V v;// 找要删除的元素,赋值给nodeif (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {node = p;} else if ((e = p.next) != null) {if (p instanceof HashMap.TreeNode) {// 从树中查找节点node = ((HashMap.TreeNode<K, V>) p).getTreeNode(hash, key);} else {// 从链表中查找节点 ,链表结构时,p是node的前置节点do {if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {// node不为空的时候,删除节点if (node instanceof HashMap.TreeNode) {((HashMap.TreeNode<K, V>) node).removeTreeNode(this, tab, movable);} else if (node == p) {tab[index] = node.next;} else {p.next = node.next;}// 修改次数加1,size减一,返回删除的node++modCount;--size;afterNodeRemoval(node);return node;}}return null;
}

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

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

相关文章

【Webpack】处理字体图标和音视频资源

处理字体图标资源 1. 下载字体图标文件 打开阿里巴巴矢量图标库open in new window选择想要的图标添加到购物车&#xff0c;统一下载到本地 2. 添加字体图标资源 src/fonts/iconfont.ttf src/fonts/iconfont.woff src/fonts/iconfont.woff2 src/css/iconfont.css 注意字体…

[计算机网络]---TCP协议

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一 、TCP协…

Java并发基础:原子类之AtomicBoolean全面解析

本文概要 AtomicBoolean类优点在于能够确保布尔值在多线程环境下的原子性操作&#xff0c;避免了繁琐的同步措施&#xff0c;它提供了高效的非阻塞算法实现&#xff0c;可以大大提成程序的并发性能&#xff0c;AtomicBoolean的API设计非常简单易用。 AtomicBoolean核心概念 …

Facebook Horizon:探索虚拟现实中的社交空间

随着科技的不断进步&#xff0c;虚拟现实&#xff08;VR&#xff09;技术正成为社交互动和娱乐体验的新前沿。在这个数字时代&#xff0c;Facebook作为全球最大的社交媒体平台之一&#xff0c;正在引领虚拟社交的新时代&#xff0c;其推出的虚拟社交平台Facebook Horizon成为了…

深入理解C语言(5):程序环境和预处理详解

文章主题&#xff1a;程序环境和预处理详解&#x1f30f;所属专栏&#xff1a;深入理解C语言&#x1f4d4;作者简介&#xff1a;更新有关深入理解C语言知识的博主一枚&#xff0c;记录分享自己对C语言的深入解读。&#x1f606;个人主页&#xff1a;[₽]的个人主页&#x1f3c4…

Imagewheel私人图床搭建结合内网穿透实现无公网IP远程访问教程

文章目录 1.前言2. Imagewheel网站搭建2.1. Imagewheel下载和安装2.2. Imagewheel网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar临时数据隧道3.2.Cpolar稳定隧道&#xff08;云端设置&#xff09;3.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 4.公网访问测…

flutter 文件上传组件和大文件分片上传

文件分片上传 资料 https://www.cnblogs.com/caijinglong/p/11558389.html 使用分段上传来上传和复制对象 - Amazon Simple Storage Service 因为公司使用的是亚马逊的s3桶 下面是查阅资料获得的 亚马逊s3桶的文件上传分片 分段上分为三个步骤&#xff1a;开始上传、上传对…

CSP-J 2023 T3 一元二次方程

文章目录 题目题目背景题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示 题目传送门题解思路总代码 提交结果尾声 题目 题目背景 众所周知&#xff0c;对一元二次方程 a x 2 b x c 0 , ( a ≠ 0 ) ax ^ 2 bx c 0, (a \neq 0) ax2bxc0,(a0)&#xff0c;可…

STM32G030C8T6:定时器1ms中断(以64MHz外部晶振为例)

本专栏记录STM32开发各个功能的详细过程&#xff0c;方便自己后续查看&#xff0c;当然也供正在入门STM32单片机的兄弟们参考&#xff1b; 本小节的目标是&#xff0c;系统主频64 MHZ,采用高速外部晶振&#xff0c;通过定时器3 每秒中断控制 PB9 引脚输出高低电平&#xff0c;从…

低代码开发:推动互联网企业数字化转型的关键因素

联网行业作为我国数字经济发展的核心驱动力&#xff0c;在推动国家数字化转型中扮演着至关重要的角色。与其他传统行业相比&#xff0c;互联网企业面临更加紧迫的数字化转型需求&#xff0c;因为它们需要不断适应快速变化的市场环境和技术趋势。 然而&#xff0c;由于互联网企业…

MFC 皮肤库配置

1.创建MFC 对话框 2.添加皮肤资源 添加资源 添加头文件 关闭SDL检测 添加静态库文件 修改字符集 添加头文件 将皮肤中的ssk文件加载到初始化实例中 > 运行即可

【寸铁的刷题笔记】树、dfs、bfs、回溯、递归(一)

【寸铁的刷题笔记】树、dfs、bfs、回溯、递归(一) 大家好 我是寸铁&#x1f44a; 总结了一篇刷题关于树、dfs、bfs、回溯、递归的文章✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 105. 从前序与中序遍历序列构造二叉树 模拟分析图 代码实现 /*** Definition for a binary tre…

HarmonyOS—添加/删除Module

Module是应用/服务的基本功能单元&#xff0c;包含了源代码、资源文件、第三方库及应用/服务配置文件&#xff0c;每一个Module都可以独立进行编译和运行。一个HarmonyOS应用/服务通常会包含一个或多个Module&#xff0c;因此&#xff0c;可以在工程中创建多个Module&#xff0…

如何利用内网穿透工具在企业微信开发者中心实现本地接口服务回调

文章目录 1. Windows安装Cpolar2. 创建Cpolar域名3. 创建企业微信应用4. 定义回调本地接口5. 回调和可信域名接口校验6. 设置固定Cpolar域名7. 使用固定域名校验 企业微信开发者在应用的开发测试阶段&#xff0c;应用服务通常是部署在开发环境&#xff0c;在有数据回调的开发场…

前端知识复习

1.symbol类型 Symbol 是 ECMAScript 6 中引入的一种新的基本数据类型&#xff0c;它表示独一无二的值。Symbol 值是通过 Symbol() 函数创建的。 Symbol 值具有以下特点&#xff1a; 独一无二性&#xff08;唯一性&#xff09;&#xff1a;每个通过 Symbol() 函数创建的 Symb…

十三:集合

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 01、Java 集合框架概述1.1、集合框架与数组的对比及概述1.2、集合框架涉及到的API 02、Collection接口方法2.1、Collection接口中的常用方法12.2、Collection接口中…

在idea中配置Tomcat

1.在idea中点击右上角 2.点击Edit Configurations,点击加号 3.向下拉找到Tomcat Server下的Local,点一下 点击Configure 找到tomcat文件路径,选择apache-tomcat-8.5.63(8.5.63是我的版本号) 选择好路径后点ok就配置好了 总步骤:

代码随想录算法训练营第一天

● 今日学习的文章链接和视频链接 ● 自己看到题目的第一想法 1. 704二分法&#xff1a; 方法一&#xff1a; 整个数组是 左闭右闭区间 [ ] left指针指向数组开始下标&#xff0c; right 指针指向数组最后下表nums.size()-1, mid为 (leftright) /2循环条件 left<rightnu…

小折叠也能成为主力机,全新小折叠旗舰华为Pocket 2正式发布

2024年2月22日&#xff0c;华为在三亚举办华为Pocket 2时尚盛典&#xff0c;正式发布其全新小折叠旗舰华为Pocket 2。一直以来&#xff0c;华为致力于萃取各界艺术灵感&#xff0c;不断探寻科技美学的可能性&#xff0c;华为Pocket系列更是秉承将奢雅美学与尖端科技融为一体的理…

探索Redis是否为单线程的奥秘(文末送书)

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、网络奇遇记 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. Redis中的多线程二. I/O多线程三. Redis中的多进程四. 结论五. 书籍推荐5.1 书…