当前位置: 首页 > news >正文

哈希表笔记(三)Java Hashmap

一、基本介绍

HashMap 是 Java 集合框架中的核心类之一,基于哈希表实现,提供了 Map 接口的主要实现。

1.1 主要特点

  • 实现了 Map<K,V> 接口
  • 允许 null 键和 null 值(不同于 Hashtable)
  • 非同步实现(非线程安全)
  • 不保证元素顺序的一致性和稳定性
  • 基本操作(get 和 put)提供常数级时间性能(O(1))
  • 迭代性能与"容量"和"大小"成正比

1.2 继承关系

java.lang.Object└── java.util.AbstractMap<K,V>└── java.util.HashMap<K,V>

1.3 常见应用场景

  • 快速查找数据:当需要通过键快速查找数据时
  • 缓存实现:保存数据以便快速访问
  • 数据统计:统计元素出现次数
  • 关联映射:需要建立对象与对象之间关系时

二、核心参数

2.1 基本参数

// 默认初始容量 - 必须是 2 的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 即 16// 最大容量(必须是 2 的幂且小于等于 1<<30)
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;

2.2 关键字段

// 存储元素的数组,长度总是 2 的幂
transient Node<K,V>[] table;// 保存 entrySet() 的缓存
transient Set<Map.Entry<K,V>> entrySet;// 键值对数量
transient int size;// 结构修改次数(用于快速失败机制)
transient int modCount;// 扩容阈值 = 容量 * 负载因子
int threshold;// 哈希表的负载因子
final float loadFactor;

三、数据结构详解

3.1 Node 节点(基本节点)

static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;// 构造器和各种方法实现...
}
  • hash: 键的哈希值
  • key: 键
  • value: 值
  • next: 指向下一个节点的引用,形成链表结构

3.2 TreeNode 节点(树形节点)

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;TreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;boolean red;// 各种红黑树操作方法...
}
  • parent: 父节点
  • left: 左子节点
  • right: 右子节点
  • prev: 双向链表中的前驱节点
  • red: 红黑树中节点的颜色标记

四、关键方法实现

4.1 哈希计算方法

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 核心思想:将 hashCode 的高 16 位和低 16 位进行异或,增加低位的随机性
  • 目的:减少哈希冲突,提高 HashMap 的性能
  • 特殊处理:null 键的哈希值为 0

4.2 确定桶索引

索引计算:(n - 1) & hash,其中 n 是 table 的长度。

  • 由于 n 总是 2 的幂,所以 (n - 1) 是一个全 1 的位掩码
  • (n - 1) & hash 等价于 hash % n,但位运算更高效
  • 巧妙利用了位运算提高性能

4.3 tableSizeFor 方法

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;
}
  • 作用:找到大于等于给定数值的最小的 2 的幂
  • 实现原理:通过一系列位操作,将最高位的 1 扩展到右侧所有位,然后加 1
  • 场景:设置初始容量时使用

4.4 put 方法实现

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 1. 如果表为空则创建if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 2. 计算索引位置,如果为空则直接插入新节点if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;// 3. 如果首节点就是要找的键,准备更新值if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))e = p;// 4. 如果是红黑树节点,使用树的方式插入else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 5. 遍历链表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;}}// 6. 如果找到匹配的键,更新值并返回旧值if (e != null) {V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}// 7. 结构修改计数加1++modCount;// 8. 判断是否需要扩容if (++size > threshold)resize();// 9. 插入后回调afterNodeInsertion(evict);return null;
}

4.5 get 方法实现

public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 1. 表不为空,长度大于0,且对应桶不为空if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 2. 检查第一个节点if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))return first;// 3. 检查剩余节点if ((e = first.next) != null) {// 如果是树节点,使用树的查找方法if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 遍历链表do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;
}

4.6 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;// 1. 计算新容量和新阈值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);}// 2. 如果新阈值为0,计算新阈值if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;// 3. 创建新表@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;// 4. 将旧表数据迁移到新表if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null; // 帮助GC// 如果桶中只有一个节点,直接放到新位置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;// 根据 (e.hash & oldCap) 决定节点去向if ((e.hash & oldCap) == 0) {// 放到低位链表(原索引)if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {// 放到高位链表(原索引+oldCap)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;
}

4.7 树化操作

final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;// 如果表太小,优先扩容而不是树化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;// 1. 将普通节点链表转为双向链表形式的TreeNode链表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);// 2. 如果转换成功,执行树化操作if ((tab[index] = hd) != null)hd.treeify(tab);}
}

4.8 红黑树分裂

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {TreeNode<K,V> b = this;// 将树节点分为低位和高位两部分TreeNode<K,V> loHead = null, loTail = null;TreeNode<K,V> hiHead = null, hiTail = null;int lc = 0, hc = 0;// 1. 遍历树节点,根据 (hash & bit) 分组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;elseloTail.next = e;loTail = e;++lc;}else {// 高位组if ((e.prev = hiTail) == null)hiHead = e;elsehiTail.next = e;hiTail = e;++hc;}}// 2. 根据节点数量决定是保持树形结构还是转回链表if (loHead != null) {if (lc <= UNTREEIFY_THRESHOLD)tab[index] = loHead.untreeify(map);else {tab[index] = loHead;if (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);}}
}

4.9 红黑树转链表

final Node<K,V> untreeify(HashMap<K,V> map) {Node<K,V> hd = null, tl = null;// 将 TreeNode 转回普通 Nodefor (Node<K,V> q = this; q != null; q = q.next) {Node<K,V> p = map.replacementNode(q, null);if (tl == null)hd = p;elsetl.next = p;tl = p;}return hd;
}

五、关键机制解析

5.1 哈希冲突解决方案

  1. 链地址法

    • 使用链表存储哈希冲突的元素
    • 当链表长度超过阈值时,转为红黑树提高效率
  2. 容量始终为 2 的幂的好处

    • 使用 (n - 1) & hash 代替 hash % n 计算索引,提高性能
    • 元素在扩容时分布更均匀(利用 hash 的高位信息)

5.2 扩容机制详解

  1. 触发条件

    • size > threshold(元素数量超过阈值)
    • 初始化时(table 为 null)
    • 树化时表太小(treeifyBin 方法中)
  2. 扩容过程

    • 容量翻倍(<<1)
    • 阈值翻倍
    • 重新哈希所有元素
  3. 元素再散列优化

    • 利用 hash & oldCap 将元素分为两组
    • 一组留在原位置(j)
    • 另一组放在新位置(j + oldCap)
    • 避免了全部重新计算索引的开销

5.3 树化与反树化机制

  1. 树化条件

    • 链表长度 >= TREEIFY_THRESHOLD(8)
    • 桶数组容量 >= MIN_TREEIFY_CAPACITY(64)
  2. 反树化(树转链表)条件

    • 树节点数量 <= UNTREEIFY_THRESHOLD(6)
    • 扩容时会触发检查
  3. 树化过程

    • 先转为双向链表形式的 TreeNode
    • 然后应用红黑树算法建树
  4. 为什么需要树化

    • 链表查找时间复杂度为 O(n)
    • 红黑树查找时间复杂度为 O(log n)
    • 当链表过长时,树结构能显著提高性能

5.4 负载因子的作用

  1. 定义

    • 负载因子 = 元素数量 / 容量
    • 默认值为 0.75
  2. 影响

    • 较小的负载因子:空间利用率低,但冲突少,查询性能好
    • 较大的负载因子:空间利用率高,但冲突多,查询性能差
    • 0.75 是时间和空间成本的折中
  3. 阈值计算

    • threshold = capacity * loadFactor
    • 当元素数量超过阈值时触发扩容

六、使用注意事项

6.1 线程安全问题

  • HashMap 非线程安全,并发环境下可能导致死循环、数据丢失等问题
  • 多线程环境应使用 ConcurrentHashMapCollections.synchronizedMap()

6.2 性能优化建议

  1. 初始容量设置

    • 如果预知数据量,合理设置初始容量可减少扩容次数
    • 初始容量 = 预期元素数量 / 负载因子
  2. 哈希码质量

    • 自定义键类型应正确实现 hashCode()equals() 方法
    • 尽量使哈希分布均匀,减少冲突
  3. 避免频繁扩容

    • 合理估计数据规模,一次性分配足够空间
    • 批量添加数据时,使用带初始容量的构造器

6.3 常见错误

  1. 可变对象做键

    • 键对象的哈希码变化会导致无法找到已存储的值
    • 键应为不可变对象,或确保哈希码不变
  2. 未正确实现 equals 和 hashCode

    • 必须同时重写这两个方法
    • 相等的对象必须有相同的哈希码
  3. 迭代时修改

    • 迭代过程中修改结构(增删元素)会触发 ConcurrentModificationException
    • 应使用迭代器的 remove 方法进行安全删除

七、JDK 8 相比 JDK 7 的改进

  1. 红黑树优化

    • JDK 7 中只使用链表解决冲突
    • JDK 8 引入红黑树结构,提高大量冲突时的性能
  2. 链表插入方式

    • JDK 7 采用头插法,扩容时可能导致循环链表
    • JDK 8 采用尾插法,避免了这个问题
  3. 哈希计算优化

    • JDK 8 的哈希算法更简单高效
    • 使用 h ^ (h >>> 16) 增加低位随机性
  4. 新增 API

    • 引入函数式接口支持:compute、merge、forEach 等
    • 提供更灵活的操作方式

八、常见面试题解析

8.1 为什么 HashMap 的容量总是 2 的幂?

  • 使用位运算 (n-1) & hash 代替取模运算 hash % n,提高效率
  • 确保元素分布更均匀,特别是在扩容时
  • tableSizeFor 方法确保容量总是 2 的幂

8.2 HashMap 和 Hashtable 的区别?

  • HashMap 允许 null 键和值,Hashtable 不允许
  • HashMap 非同步(非线程安全),Hashtable 同步(线程安全)
  • HashMap 性能更好,Hashtable 操作有同步开销
  • HashMap 继承自 AbstractMap,Hashtable 继承自 Dictionary
  • HashMap 的迭代器是 fail-fast 的

8.3 HashMap 的 put 过程?

  1. 计算键的哈希值
  2. 确定桶索引位置
  3. 遍历该桶,查找是否已存在该键
    • 如果桶为空,直接插入
    • 如果找到相同键,更新值
    • 如果是链表,遍历查找;达到树化阈值则树化
    • 如果是红黑树,按树的方式操作
  4. 检查是否需要扩容

8.4 HashMap 的扩容过程?

  1. 容量翻倍(新容量 = 旧容量 * 2)
  2. 计算新的阈值(新阈值 = 新容量 * 负载因子)
  3. 创建新的哈希表
  4. 将原哈希表中的所有元素重新哈希到新表
    • 巧妙利用 hash & oldCap == 0 判断元素新位置
    • 元素要么在原位置,要么在原位置 + oldCap 的位置

8.5 HashMap 如何解决哈希冲突?

  1. 链地址法:同一个桶存放链表
  2. 当链表过长时(>= 8),转换为红黑树
  3. 桶容量小于 64 时优先扩容而非树化
  4. 当树节点较少时(<= 6),退化为链表
  5. 使用高质量的哈希函数减少冲突可能性

九、源码学习收获与思考

9.1 数据结构的选择权衡

HashMap 结合使用了数组、链表和红黑树三种数据结构,根据实际情况动态调整:

  • 数组提供 O(1) 的访问性能
  • 链表适合少量元素的冲突解决
  • 红黑树处理大量冲突时保证 O(log n) 的性能

这种灵活组合的思想值得借鉴,根据实际场景选择最合适的数据结构。

9.2 扩容策略的优化思路

HashMap 的扩容过程中,元素再散列的优化非常巧妙:

  • 利用哈希值与旧容量的位运算,快速确定元素新位置
  • 只需比较一位即可决定元素去向,避免重新计算全部哈希索引
  • 这种技巧在需要频繁迁移数据的场景中非常有价值

9.3 参数选择的启示

阈值的选择(如 8 和 6)经过了精心考量:

  • TREEIFY_THRESHOLD = 8:根据泊松分布,链表长度达到 8 的概率已经非常小
  • UNTREEIFY_THRESHOLD = 6:小于树化阈值,形成迟滞效应,避免频繁树化/反树化
  • 这提醒我们在设计系统时,参数选择应基于数学模型和实际测试

9.4 面向接口设计

HashMap 通过一系列钩子方法(如 afterNodeInsertion)支持子类扩展:

  • 这些方法在基类中是空实现
  • 子类如 LinkedHashMap 可以重写这些方法实现特定功能
  • 很好地体现了开闭原则和模板方法设计模式

十、总结

关键要点:

  1. 基于数组 + 链表 + 红黑树的复合结构
  2. 容量总是 2 的幂,便于哈希计算
  3. 负载因子影响空间利用率与性能
  4. 树化/反树化机制动态优化性能
  5. 巧妙的扩容算法降低重哈希成本
  6. 非线程安全,并发使用需注意

HashMap 桶数据结构与扩容机制的代码详解

桶的数据结构定义

HashMap 的桶结构是通过 Node 类和 TreeNode 类实现的:

基本节点 - Node 类
// 基本的哈希桶节点,用于大多数条目
static class Node<K,V> implements Map.Entry<K,V> {final int hash;     // 哈希值final K key;        // 键V value;            // 值Node<K,V> next;     // 指向下一个节点Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}// 实现 Map.Entry 接口的方法...
}
树节点 - TreeNode 类
// 树形节点,当桶中元素过多时使用
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // 父节点TreeNode<K,V> left;    // 左子节点TreeNode<K,V> right;   // 右子节点TreeNode<K,V> prev;    // 用于删除时解链boolean red;           // 红黑树颜色标记TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}// 红黑树相关操作方法...
}
桶数组定义
// 桶数组,首次使用时初始化,必要时调整大小
transient Node<K,V>[] table;

扩容机制的具体实现

扩容通过 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;}// 否则容量翻倍(左移1位),阈值也翻倍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; // 帮助GCif (e.next == null) // 单个节点直接放入新位置newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode) // 树节点需要特殊处理((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // 链表节点,根据 (e.hash & oldCap) 拆分为两部分Node<K,V> loHead = null, loTail = null;  // 原索引Node<K,V> hiHead = null, hiTail = null;  // 原索引+oldCapNode<K,V> next;do {next = e.next;// 根据新增的位判断节点去向if ((e.hash & oldCap) == 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;
}

链表转树和树转链表(缩容相关)

链表转树通过 treeifyBin() 方法:

final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;// 如果桶数组太小,优先扩容而不是树化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;// 将链表转换为双向链表形式的TreeNodedo {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);// 对双向链表进行树化操作if ((tab[index] = hd) != null)hd.treeify(tab);}
}

树转链表在 split() 方法中实现:

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {TreeNode<K,V> b = this;// 将树拆分为两部分TreeNode<K,V> loHead = null, loTail = null;TreeNode<K,V> hiHead = null, hiTail = null;int lc = 0, hc = 0;// 遍历树节点,根据 hash & bit 分为两组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) {// 放入 lo 组if ((e.prev = loTail) == null)loHead = e;elseloTail.next = e;loTail = e;++lc;}else {// 放入 hi 组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)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);}}
}

树转链表的 untreeify() 方法:

final Node<K,V> untreeify(HashMap<K,V> map) {Node<K,V> hd = null, tl = null;// 将 TreeNode 转为普通 Nodefor (Node<K,V> q = this; q != null; q = q.next) {Node<K,V> p = map.replacementNode(q, null);if (tl == null)hd = p;elsetl.next = p;tl = p;}return hd;
}

相关文章:

哈希表笔记(三)Java Hashmap

一、基本介绍 HashMap 是 Java 集合框架中的核心类之一&#xff0c;基于哈希表实现&#xff0c;提供了 Map 接口的主要实现。 1.1 主要特点 实现了 Map<K,V> 接口允许 null 键和 null 值&#xff08;不同于 Hashtable&#xff09;非同步实现&#xff08;非线程安全&am…...

2025五一杯C题五一杯数学建模思路代码文章教学:社交媒体平台用户分析问题

完整内容请看文章最下面的推广群 问题一详细分析&#xff1a;逐步思考与建模过程 第1步&#xff1a;问题理解与数学建模目标明确 目标明确&#xff1a; 平台希望根据2024年7月11日至7月20日的用户与博主交互数据&#xff0c;预测2024年7月21日各博主新增的关 注数&#xff0c;…...

2025五一数学建模竞赛B题完整分析论文(共42页)(含模型、可运行代码、数据)

2025年五一数学建模竞赛B题完整分析论文 摘 要 一、问题分析 二、问题重述 三、模型假设 四、符号定义 五、 模型建立与求解 5.1问题1 5.1.1问题1思路分析 5.1.2问题1模型建立 5.1.3问题1代码 5.1.4问题1求解结果 5.2问题2 5.2.1问题2思路分析 5.2.…...

[蓝桥杯 2021 省 AB] 砝码称重 Java

import java.util.*;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);int n sc.nextInt();int[] w new int[n 1];int sum 0; // 所有砝码重量之和for (int i 1; i < n; i) {w[i] sc.nextInt();sum w[i];}sc.close()…...

Linux 命令如何同时支持文件参数与管道输入?

文章目录 Linux 命令如何同时支持文件参数与管道输入&#xff1f;命令输入方式与管道机制概述常见输入控制方式常见使用示例程序实现思路&#xff1a;统一处理输入的方式判定输入来源的基本模式为何命令应支持参数与标准输入&#xff1f; GNU Coreutils wc 源码解析&#xff1a…...

Lucene多种数据类型使用说明

Lucene 作为一款高性能的全文检索引擎库&#xff0c;其核心功能围绕索引和搜索文本数据&#xff0c;但它也支持多种数据类型以满足复杂的应用场景。以下是 Lucene 支持的主要数据类型及其用途的详细说明&#xff1a; 1. 文本类型&#xff08;Text&#xff09; 用途&#xff1a;…...

使用CubeMX新建DMA工程——存储器到外设模式

目录 1、新建板级支持包 1、usart.c: 2、修改的地方&#xff1a;在usart.c中添加了这些 3、usart.h: 4、在usart.h中添加了这些&#xff1a; 5、dma.c: 6、dma.h: 2、修改main.c文件 1、在main.c文件中添加头文件 2、添加外部变量声明 3、添加简单延时函数 4、添加…...

基于Redis实现-附近商铺查询

基于Redis实现-附近查询 这个功能将使用到Redis中的GEO这种数据结构来实现。 1.GEO相关命令 GEO就是Geolocation的简写形式&#xff0c;代表地理坐标。Redis在3.2版本中加入到了对GEO的支持&#xff0c;允许存储地理坐标信息&#xff0c;帮助我们根据经纬度来检索数据&#…...

【Redis】Another Redis Desktop Manager 安装指南

目录 一、Another Redis Desktop Manager 介绍 ​二、下载安装指南 三、效果预览 一、Another Redis Desktop Manager 介绍 国人开发的更快、更好、更稳定的 Redis 桌面客户端工具&#xff0c;功能强大&#xff0c;支持集群、哨兵、SSL 认证、 树状视图、自定义格式化脚本等功…...

装饰器设计模式(Decorator Pattern)详解

装饰器设计模式(Decorator Pattern)详解 装饰器模式是一种结构型设计模式,它允许动态地向对象添加额外行为,而无需修改其原始类。这种模式通过包装对象的方式提供灵活的扩展功能替代继承。 1. 核心概念 (1)模式定义 装饰器模式:动态地给一个对象添加一些额外的职责,就…...

LiteOS与SLE透传实战案例

文章目录 硬件设计EDA 软件介绍创建元件及封装原理图绘制Layout元件焊接 软件设计LiteOS 入门核心概念TaskWorkflow参考 API&#xff08;参考 osal_task. h&#xff09; 时间片任务轮转练习&#xff1a;仿写 example/peripheral/blinkyQueue参考 API&#xff08;参考 osal_msgq…...

MCAL学习(1)——AutoSAR

1.了解AutoSAR及一些概念 AutoSAR是Automotive Open System Architecture ,汽车开放系统架构。 针对汽车ECU的软件开发架构。已经是汽车电子软件开发的标准。 OS服务&#xff1a;Freertos 整车厂&#xff08;OEM&#xff09;主要负责应用层算法 一级供应商&#xff1a;生产制…...

前端vue3项目学习

鸽王经过一个多月的学习&#xff08;断断续续吧&#xff0c;毕竟还有其他杂事&#xff09;&#xff0c;学的昏天黑地&#xff0c;终于把主线捋的差不多了。只能说&#xff0c;前端真不是人学的&#xff0c;涉及的语言语法太杂乱了&#xff0c;入门真的太难了。而后端&#xff0…...

ActiveMQ 性能优化与网络配置实战(二)

五、性能优化实战 5.1 基础配置调整 5.1.1 增加并发消费者 在 ActiveMQ 中&#xff0c;增加并发消费者是提高消息处理效率的重要手段之一。通过配置多个消费者并行处理消息&#xff0c;可以充分利用系统资源&#xff0c;加快消息的消费速度&#xff0c;从而提高系统的整体吞…...

Python 函数装饰器和闭包(装饰器基础知识)

本章内容&#xff1a; Python 如何计算装饰器句法 Python 如何判断变量是不是局部的 闭包存在的原因和工作原理 nonlocal 能解决什么问题 掌握这些基础知识后&#xff0c;我们可以进一步探讨装饰器&#xff1a; 实现行为良好的装饰器 标准库中有用的装饰器 实现一个参数化装饰器…...

“Everything“工具 是 Windows 上文件名搜索引擎神奇

01 Everything 和其他搜索引擎有何不同 轻量安装文件。 干净简洁的用户界面。 快速文件索引。 快速搜索。 快速启动。 最小资源使用。 轻量数据库。 实时更新。 官网&#xff1a;https://www.voidtools.com/zh-cn/downloads/ 通过网盘分享的文件&#xff1a;Every…...

【Machine Learning Q and AI 读书笔记】- 04 彩票假设

Machine Learning Q and AI 中文译名 大模型技术30讲&#xff0c;主要总结了大模型相关的技术要点&#xff0c;结合学术和工程化&#xff0c;对LLM从业者来说&#xff0c;是一份非常好的学习实践技术地图. 本文是Machine Learning Q and AI 读书笔记的第4篇&#xff0c;对应原…...

linux下安装ollama网不好怎么办?

文章目录 前言kkgithub下载脚本,而不是直接运行修改脚本修改权限还是不行?前言 今天想在linux上面更新一下ollama,于是去到官网: https://ollama.com/download/linux linux下安装ollama还是挺简单的: curl -fsSL https://ollama.com/install.sh | sh我也是特别嗨皮地就…...

(A题|支路车流量推测问题)2025年第二十二届五一数学建模竞赛(五一杯/五一赛)解题思路|完整代码论文集合

我是Tina表姐&#xff0c;毕业于中国人民大学&#xff0c;对数学建模的热爱让我在这一领域深耕多年。我的建模思路已经帮助了百余位学习者和参赛者在数学建模的道路上取得了显著的进步和成就。现在&#xff0c;我将这份宝贵的经验和知识凝练成一份全面的解题思路与代码论文集合…...

RDMA高性能网络通信实践

RDMA高性能网络通信实践 一、背景介绍二、方法设计A.实现方案B.关键技术点三、代码及注释四、注意事项一、背景介绍 远程直接内存访问(RDMA)技术通过绕过操作系统内核和CPU直接访问远程内存,实现了超低延迟、高吞吐量的网络通信。该技术广泛应用于高性能计算、分布式存储和…...

【LeetCode Hot100】图论篇

前言 本文用于整理LeetCode Hot100中题目解答&#xff0c;因题目比较简单且更多是为了面试快速写出正确思路&#xff0c;只做简单题意解读和一句话题解方便记忆。但代码会全部给出&#xff0c;方便大家整理代码思路。 200. 岛屿数量 一句话题意 求所有上下左右的‘1’的连通块…...

图论---有向图的强连通分量(Tarjan求SCC)

强连通分量作用&#xff1a;有向图——>&#xff08;缩点&#xff09;有向无环图&#xff08;DAG&#xff09; 缩点&#xff1a;将所有连通的分量缩成一个点。 有向无环图作用/好处&#xff1a;求最短路/最长路 可以递推&#xff0c;按照拓扑图从前往后递推. x 是否在某个…...

2025年- H17-Lc125-73.矩阵置零(矩阵)---java版

1.题目描述 2.思路 &#xff08;1&#xff09;计算矩阵的行数 &#xff08;2&#xff09;计算矩阵的列数 &#xff08;3&#xff09;设计一个行列的bool数组 &#xff08;4&#xff09;遍历矩阵&#xff08;二维数组&#xff09;&#xff0c;如果遇到元素0&#xff0c;则把…...

【信息系统项目管理师-论文真题】2023下半年论文详解(包括解题思路和写作要点)

更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 试题(第一批):论信息系统项目的干系人管理1、写作要点2、解题思路项目干系人管理的过程和执行要点项目中所有干系人如何进行分类管理?试题(第二批):论信息系统项目的工作绩效域1、写作要点2、解题思路绩…...

分享5款开源、美观的 WinForm UI 控件库

前言 今天大姚给大家分享5款开源、美观的 WinForm UI 控件库&#xff0c;助力让我们的 WinForm 应用更好看。 WinForm WinForm是一个传统的桌面应用程序框架&#xff0c;它基于 Windows 操作系统的原生控件和窗体。通过简单易用的 API&#xff0c;开发者可以快速构建基于窗体…...

微软推出数款Phi 4“开放式”人工智能模型

微软周三推出了几款新的“开放式”人工智能模型&#xff0c;其中功能最强大的模型至少在一个基准测试上可与 OpenAI 的 o3-mini 相媲美。所有新的授权模型——Phi 4 mini reasoning、Phi 4 reasoning 和 Phi 4 reasoning plus——都是“推理”模型&#xff0c;这意味着它们能够…...

Python实例题:Python实现Python解释器

目录 Python实例题 题目 实现思路 代码实现 代码解释 词法分析器&#xff08;Lexer&#xff09;&#xff1a; 词法单元类&#xff08;Token&#xff09;&#xff1a; 抽象语法树节点类&#xff08;AST&#xff09;&#xff1a; 语法分析器&#xff08;Parser&#xff…...

【IP101】图像滤波技术详解:从均值滤波到高斯滤波的完整指南

&#x1f31f; 图像滤波魔法指南 &#x1f3a8; 在图像处理的世界里&#xff0c;滤波就像是给图片"美颜"的魔法工具。让我们一起来探索这些神奇的滤波术吧&#xff01; &#x1f4d1; 目录 1. 均值滤波&#xff1a;图像的"磨皮"大法2. 中值滤波&#xff1…...

信息系统项目管理师-软考高级(软考高项)​​​​​​​​​​​2025最新(六)

个人笔记整理---仅供参考 第六章项目管理概论 6.1PMBOK的发展 6.2项目基本要素 组织过程资产指的是项目上的&#xff0c;国产数据库的使用----安保和安全指的是环境因素 6.3项目经理的角色 6.4价值驱动的项目管理知识体系...

算法-堆、排序算法、矩阵乘法

满二叉树、完全二叉树 二叉树遵循下面的规律&#xff0c;当前节点i&#xff08;但是其实就是逐级填充&#xff09;: 左节点为 ix2右节点为 i*21父节点为 [i/2] 向下取整 使用数组表示二叉树&#xff1a; &#xff08;二叉树的深度自上而下&#xff0c;高度自下…...

Java 进阶--集合:告别数组的“僵硬”,拥抱灵活的数据容器

作者&#xff1a;IvanCodes 发布时间&#xff1a;2025年5月1日&#x1fae1; 专栏&#xff1a;Java教程 大家好&#xff01;&#x1f44b; 还记得我们上次聊的数组 (Array) 吗&#xff1f;它很基础&#xff0c;性能也不错&#xff0c;但有个致命的缺点&#xff1a;长度一旦定…...

Python 数据智能实战 (6):用户评论深度挖掘

写在前面 —— 从繁杂评论到精准洞察:主题发现与情感趋势分析,驱动产品优化与体验提升 在上篇内容中,我们学习了如何利用 LLM 提升用户分群的精度,以及如何超越传统购物篮分析,挖掘商品间的语义关联。今天,我们将聚焦于电商领域 价值密度最高 的非结构化数据之一——用…...

podman/docker国内可用的docker镜像源(2025-05)

一、添加Docker国内镜像 1、修改 /etc/docker/daemon.json 设置 registry mirror&#xff0c;具体命令如下: sudo vim /etc/docker/daemon.json <<EOF {"registry-mirrors": ["https://docker.1ms.run","https://docker.xuanyuan.me",&q…...

机器人--底盘

机器人底盘 底盘是机器人传感器和机器人主机的载体&#xff0c;也是移动机器人的基本形式。移动机器人通常可以采用轮式和足式进行移动。 也就是机器人底盘可以分为轮式底盘和足式底盘。 足式底盘控制复杂&#xff0c;这里只讨论轮式底盘。 底盘运动学模型 轮式机器人底盘按…...

Seata服务端同步提交事务核心源码解析

文章目录 前言一、doGlobalCommit&#xff08;同步提交&#xff09;2.1、closeAndClean()2.2、changeGlobalStatus2.3、doGlobalCommit2.3.1、findGlobalSession 总结 前言 本篇介绍Seata服务端TC如何驱动RM提交事务。 一、doGlobalCommit&#xff08;同步提交&#xff09; doG…...

2025五一杯B题五一杯数学建模思路代码文章教学: 矿山数据处理问题

完整内容请看文章最下面的推广群 问题1. 根据附件1中的数据和&#xff0c;建立数学模型&#xff0c;对数据A进行某种变换&#xff0c;使得变换后的结果与数据尽可能接近。计算变换后的结果与数据的误差&#xff0c;并分析误差的来源&#xff08;如数据噪声、模型偏差等&#xf…...

C++11新特性_自动类型推导

decltype 和 auto 均为 C 里用于类型推导的关键字&#xff0c;不过它们在使用方式、推导规则和应用场景上存在显著差异。下面为你详细介绍它们的区别&#xff1a; 1. 推导依据 auto&#xff1a;它依据变量的初始化表达式来推导类型。也就是说&#xff0c;auto 定义的变量必须有…...

【AI论文】ReasonIR:为推理任务训练检索器

摘要&#xff1a;我们提出了ReasonIR-8B&#xff0c;这是第一个专门针对一般推理任务进行训练的检索器。 现有的检索器在推理任务上表现出的收益有限&#xff0c;部分原因是现有的训练数据集侧重于与直接回答它们的文档相关的简短事实查询。 我们开发了一个合成数据生成管道&am…...

嵌入式AI还是一片蓝海

发现其实还是挺多人关注嵌入式和人工智能交叉领域的&#xff0c;随便一个问题&#xff0c;浏览量就27万了&#xff0c;但是这方面的内容确实少得可怜……所以干脆我自己来补点干货。 推荐一本最近很热门的新书——《边缘人工智能&#xff1a;用嵌入式机器学习解决现实问题》。 …...

日语学习-日语知识点小记-构建基础-JLPT-N4阶段(13): ておきます ています & てあります

日语学习-日语知识点小记-构建基础-JLPT-N4阶段&#xff08;13&#xff09;&#xff1a; ておきます &ています &#xff06; てあります 。 1、前言&#xff08;1&#xff09;情况说明&#xff08;2&#xff09;工程师的信仰 2、知识点&#xff08;1&#xff09;&#x…...

CMake管理外部依赖的模块

在 CMake 中&#xff0c;FetchContent 和 ExternalProject 都是管理外部依赖的模块&#xff0c;但它们的 设计目标、使用场景和执行时机 有本质区别。以下通过对比表格、代码示例和场景分析详细说明它们的区别。 核心区别对比表 特性FetchContentExternalProject执行阶段配置阶…...

[计算机科学#7]:CPU的三阶段,取指令、解码、执行

【核知坊】&#xff1a;释放青春想象&#xff0c;码动全新视野。 我们希望使用精简的信息传达知识的骨架&#xff0c;启发创造者开启创造之路&#xff01;&#xff01;&#xff01; 内容摘要&#xff1a;本文详细介绍了CPU的工作原理&#xff0c;包括其结构…...

向量数据库和关系型数据库的区别,优点,缺点和典型应用场景

向量数据库与关系型数据库的全面对比 向量数据库和关系型数据库是两种截然不同的数据管理系统&#xff0c;各自针对特定的数据模型和查询模式进行了优化。随着人工智能和大数据技术的发展&#xff0c;向量数据库作为新兴的数据库类型&#xff0c;在处理非结构化数据方面展现出…...

《跨越边界:探索跨端框架中通用状态管理方案设计》

一款应用往往需要在多个终端&#xff0c;如Web、移动端、桌面端等同时运行&#xff0c;以满足用户多元化的使用场景。在这复杂的跨端开发领域中&#xff0c;状态管理堪称关键枢纽&#xff0c;直接关乎应用的性能、稳定性以及开发与维护的效率。如何设计一套通用的状态管理方案&…...

PHP之CURL通过header传参数及接收

一、传参数之冒号 注意一点&#xff0c;这里的header数据不是KV结构&#xff0c;而是一个一维数组。 看清楚&#xff0c;注意一点&#xff0c;是这样的结构&#xff1a; $ch curl_init(); $headers [X-Custom-Header: value123,Authorization: Bearer your_token_here // …...

【C++】brpc安装

brpc安装教程 环境&#xff1a;Ubuntu24.04 1 简单安装 即安装到系统环境下&#xff0c;依赖也是依赖apt安装。 官方参考教程 依赖准备 安装依赖&#xff1a; sudo apt-get install -y git g make libssl-dev libgflags-dev libprotobuf-dev libprotoc-dev protobuf-com…...

从0开始的c++知识讲解之字符串(1)

作者作为新手&#xff0c;对于知识的讲解也是边输出内容也是边学习&#xff0c;如有缺陷&#xff0c;请多海涵&#xff0c;但同样&#xff0c;我会帮助你从新手视角看到新手的疑惑&#xff0c;并帮助你解决此疑惑 一&#xff0c;开宗明义&#xff0c;立意先行 string在C里有可…...

Linux 第六讲 --- 工具篇(一)yum/apt与vim

前言&#xff1a; 经过前五讲对Linux基础指令与权限系统的系统学习&#xff0c;相信你已经能在命令行中自如地穿梭于文件丛林&#xff0c;精准调配权限密钥。但真正的Linux玩家&#xff0c;绝不会止步于基础操作的重复劳作。 从今天起&#xff0c;我们将打开Linux的"瑞士…...

xml 和 yaml 的区别

XML 和 YAML/YML 是两种常用的数据序列化格式&#xff0c;用于存储和读取结构化数据。以下是它们的核心区别和使用方法&#xff1a; 1. 格式特性对比 特性XMLYAML/YML语法复杂度标签嵌套&#xff0c;结构严格缩进分层&#xff0c;更简洁可读性较低&#xff08;冗余标签&#…...

1.67g 雨晨 22635.5305 Windows 11 企业版 23H2 极速增强版

五一特别制作 &#xff08;主要更新简述&#xff09; 全程由最新YCDISM2025装载制作 1、可选功能&#xff1a; 添加&#xff1a; Microsoft-Windows-LanguageFeatures-Basic-en-us-Package Microsoft-Windows-LanguageFeatures-OCR-en-us-Package 2、功能增强&a…...