内容说明:本文着重分析 putVal() ,resize(),putTree() 操作
简单分析了平衡操作,详细的平衡情况,见The Tree (AVL, 2-3-, 红黑,Huffman)
最后解读了 get(),和 remove() 的代码
removeTreeNode() 部分交换右子树而非左子树的操作未理解
1. HashMap 类的使用方式
public class HashMapDemo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("a",90);
map.put("b",85);
map.put("c",88);
map.put("d",92);
Integer integer = map.get("d");
Integer s = map.remove("s");
System.out.println(s);
Set<String> keys = map.keySet();
for(String key : keys){
System.out.print(key + " ");
}
System.out.println();
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for(Map.Entry<String, Integer> entry : entries){
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("key = " + key + " value = " + value);
}
map.forEach((key, value) ->{
System.out.println("key = " + key + " value = " + value);
});
}
}
2. HashMap 相关接口
2.1 Map 接口
public interface Map<K,V> {
// 返回键值对的数量,最大值为 Integer.MAX_VALUE
int size();
// 若map为空返回 true
boolean isEmpty();
// 判断映射是否包含给定的键
boolean containsKey(Object key);
// 判断映射是否包含给定的值
boolean containsValue(Object value);
// 获取键对应的值,返回null也可能是key被映射为null
V get(Object key);
// 在映射中添加键值对,若键已存在则更新值
V put(K key, V value);
// 删除键对应的键值对
V remove(Object key);
// 将指定的Map 复制到当前map 中
void putAll(Map<? extends K, ? extends V> m);
// 清空映射集合
void clear();
// 返回map中所有键的 Set 视图
Set<K> keySet();
// 返回map中所有值的 Set 视图
Collection<V> values();
// 返回map中所有键值对的 Set 视图
Set<Map.Entry<K, V>> entrySet();
boolean equals(Object o);
int hashCode();
...
2.2 Entry 接口
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
3. HashMap 类属性
// 默认初始容量,16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
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;
// 树化前table的最小扩容值
static final int MIN_TREEIFY_CAPACITY = 64;
// Node 数组
transient Node<K,V>[] table;
// map 用于保存键值对的集合
transient Set<Map.Entry<K,V>> entrySet;
// 键值对的数量
transient int size;
// map 发生结构性变化[put,remove...]的次数
transient int modCount;
// size 的阈值,size = capacity * loadfactor
int threshold;
// 负载因子
final float loadFactor;
4. HashMap 静态内部类
4.1 Node
HashMap 的 table 属性是一个 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;
}
}
4.2 TreeNode
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
// true - 红,false - 黑
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
TreeNode 继承了 LinkedHashMap.Entry
5. HashMap 类构造器
① 指定初始容量initialCapacity
,负载因子loadFactor
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
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
③ 无参构造器
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
④ 传入Map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
6. put()
put() 方法将键值对存放在map中,若map中已包含与该键对应的键值对,则会更新该键值对的值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
思考:hashMap 链式存储结构是怎么来的?
元素是 Node,有 next 结点,因此可以形成链表
那么如何计算链表的位置呢?,是根据Node 结点吗?
从这里来看,是根据 node 结点中的 key 属性,计算出 hash 值,将其作为参数调用 putVal(args…) 方法
6.1 hash(Object key)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
返回 key 对应的 hash值
① key == null
返回 0
② key != null
返回 key 的hash值高16位与低16位相与的结果
计算 key 的hash 值是调用 Object 中的 hashCode() 方法
6.1.1 hashCode()
public native int hashCode();
6.2 putVal(arg…)
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;
}
画完图之后,突然想明白一个点:直接==
比较不相等后,继续调用 key.equals(k)
比较?
(k = p.key) == key || (key != null && key.equals(k))
之前每次看,都觉得 k == key
和 key == k
不是一样的吗?为什么要比较两次
public boolean equals(Object obj) {
return (this == obj);
}
key 为Map<K,V>
中K对应的对象,K 会重写 equals(obj) 方法
以 K instanceof Integer
为例
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
Integer 重写的 equals(obj) 方法会进行类型转换,获取对象的值比较
问题:根据存放的key计算出hash,出现哈希冲突,遍历链表时根据binCount
判断是否需要树化,好像跟数组的大小无关
验证下 树化的最小数组容量阈值为MIN_TREEIFY_CAPACITY = 64
的说法是否正确
在后续的 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();
...
}
上述添加元素的操作流程总结:
① map.put(key,value)
,根据 hash(key)
计算出hash
值,然后调用 map.putVal(hash, key, value, false, true) 添加键值对
② map.putVal(int, K, V, boolean, boolean)
,先判断哈希表是否为空或长度为0,是则调用 resize()
进行扩容
③ (n - 1) & hash
计算待存放node在哈希表中的位置tab[i]
,若该位置上没有元素,则将 <key,value>
封装到node
中并存放到 tab[i]
位置上
④ 若当前位置上有元素,分为三种情况考虑:
-
tab[i]
处结点p
的hash
值和key
值与待插入元素相同,即key的更新操作,直接获取此时的p
-
上述
1.
中的条件不满足,则判断是否为树结点,是则执行putTreeVal(...)
操作 -
上述
1,2
均不满足,则利用拉链法在tab[i]
位置的链表上寻找合适的位置
遍历链表的过程中仍执行1.
判断是否为链表上某个结点的更新操作,不是则插到链表的末尾,并判断是否需要树化
⑤ e != null
更新操作才满足条件,将oldValue
替换为新值,由于不涉及结构性变化不会执行⑥
否则 e == null
,已经在对应位置插入了new node 或 new TreeNode
⑥ 插入操作,判断容量是否大于阈值,是则扩容,并返回null
,旧值不存在
6.2.1 resize()
问题:调用指定初始容量的构造器,若initialCapacity = 0
时会怎样?
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
该构造器会调用HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity, float loadFactor) {
...
this.threshold = tableSizeFor(initialCapacity);
}
根据tableSizeFor(initialCapacity)
计算阈值threshold
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;
}
若 cap = 0
,则n = FFFFFFF
,无符号右移,n |= n >>> x
结果还是-1
当 cap = 0
时,threshold = 1
上述代码中
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
newCap < MAXIMUM_CAPACITY
后还有必要判断ft < (float)MAXIMUM_CAPACITY
吗?
若 loadFactor < 1
则ft < newCap
那么threashold > 1
会怎样?ft > newCap
需要进一步判断ft < (float)MAXIMUM_CAPACITY
负载因子可在创建 HashMap 对象时手动设置
public HashMap(int initialCapacity, float loadFactor) {
...
}
代码中并没有关于 loadFactor
的限制,上述博文提到,若loadFactor > 1
即使数组满了,也不会扩容,直到达到计算的阈值【数组容量 * 负载因子】
流程图的上半部分主要是扩容操作:设置 newCap
和 newThr
下半部分主要是元素转移:即根据 hash 值将元素分布到扩容后的数组中
关于上图中低段和高段的说明:以 oldTab.size = 16 为例,扩容后 newTab.size = 32
若e.hash & (newCap - 1) == 1
该hash值对应的链表就从低16位搬迁到高16位的对应位置上了
从上面也可以看出:高位为 1 扩容后迁移到扩容的高位部分,不是人为规定,而是计算结果必然
6.3 链表 <--->
红黑树
6.3.1 treeifyBin()
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 1. 若数组为空,或者 数组的长度小于 64 则扩容,不执行树化操作
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 2. tab 当前位置非空
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 设置与树结点对应的双向链表的首部和尾部
TreeNode<K,V> hd = null, tl = null;
do {
// 2.1 将 node 结点转化为 treeNode 结点
TreeNode<K,V> p = replacementTreeNode(e, null);
// 2.2 若双向链表为空,则设置 p 为头结点
if (tl == null)
hd = p;
// 2.3 双向链表非空,则将p插入到链表尾部
else {
p.prev = tl;
tl.next = p;
}
// 2.4 更新尾结点
tl = p;
} while ((e = e.next) != null);
// 2.5
if ((tab[index] = hd) != null)
hd.treeify(tab);
不是很理解上面:执行 hd.treeify(tab)
之前为什么还要判断 (tab[index]] = hd) != null
的操作
hashmap 中用到 treeify(tab) 的代码共有三处:putVal(),merge(),computeIfAbsent()
但都是判断 binCount >= TREEIFY_THRESHOLD - 1
成立后才执行的操作,也即 tab 位置上必然有元素
则 hd = p != null 不是必然成立吗?难道有考虑多线程?可是 tab 并没有设置为 volatile 类型的
总结:链表到红黑树:链表结点Node --> 树节点TreeNode --> 双向链表 --> 树化
问题:双向链表的作用?
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
① TreeNode 继承了 LinkedHashMap 的内部类 Entry,包含 before
和after
属性,具有双向链表的属性
② 调整红黑树使其平衡时,root可能会变更,需要维护root在哈希桶上属于首元素位置。
当 root 不在哈希桶上时,移动root在双链表中的位置至首结点,并将该首结点放到哈希表上。
也即哈希表上以双链表的形式挂载,而在操作时是操作红黑树,因为链表首结点指向红黑树的root结点
而在退化回链表时,也是通过遍历双链表,形成单链表的形式
6.3.2 untreeify(HashMap<K,V> map)
final Node<K,V> untreeify(HashMap<K,V> map) {
// 1. 设置普通Node 结点对应的单链表的首和尾
Node<K,V> hd = null, tl = null;
// 2. 遍历TreeNode 结点
for (Node<K,V> q = this; q != null; q = q.next) {
// 3. 将 TreeNode 结点替换成 Node 结点
Node<K,V> p = map.replacementNode(q, null);
// 4. 单链表为空,则设置 p 为头结点
if (tl == null)
hd = p;
else
// 5. 否则将 p 连接到单链表末尾
tl.next = p;
// 6. 更新尾结点
tl = p;
}
// 7. 返回单链表头结点
return hd;
}
untreeify 是 静态内部类 TreeNode
中的 final 方法,代码中的 this
是 TreeNode 类型的结点
Node<K,V> q = this
实际上经过了如下的向上转型
TreeNode<K,V>
—>LinkedHashMap.Entry<K,V>
—>HashMap.Node<K,V>
总结:红黑树到单链表:双链表节点 -->TreeNode 树结点 —> 普通 Node 结点 —> 单链表
6.3 putTreeVal(arg…)
hashMap 中调用 putTreeVal(arg…) 的共有4处:putVal(…), computeIfAbsent(), compute(), merge() 其中后面三个函数不被hashmap中的其他函数调用
现仅考虑由 putVal() 调用 putTreeVal() 的情况
① 产生 哈希冲突
② 且 tab 位置上的元素为 TreeNode 类型
调用过程:
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
传输参数:this -- p
而调用下面的 putTreeVal 时,传入的参数 this 为 HashMap<K,V>
类型的
此处经过的向上转型过程为:TreeNode<K,V>
–>LinkedHashMap
–>HashMap
传入的参数 p 为红黑树存储在哈希表中的根结点
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;
// 1. 获取红黑树根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
// 2. 搜索红黑树
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
// 2.1 找到 k 对应的节点则返回
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 2.2 ph == h && pk != k p的hash值等于待插入结点的hash值但 key 值不相同
// 获取 k 的类型 kc, 若 kc != null 则比较p 和待插入结点的键 pk 和 k
else if ((kc == null &&
// 2.2.1 comparableClassFor(k) 返回 k 的class 类型
// 若 kc 不是"class C implements Comparable<C>"的形式,即kc不可比较,则返回null
// 2.2.2 compareComparables(kc,k,pk)若 pk 不是 kc 类型的,则返回 0
// 若 kc 无法同 k 比较,或 比较结果为 0 则到左右子树中查找该键,找到即返回
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
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;
}
dir = tieBreakOrder(k, pk);
}
// 2.2 未找到 k 对应的节点则创建新的节点
TreeNode<K,V> xp = p;
// 此处包含 for 循环的控制条件, dir <= 0 时, p = p.left, 否则 p = p.right
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
// 2.2.1 创建一个新的 TreeNode 结点
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
// 2.2.2 建立父节点和新节点的关联
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
// 2.2.3 balanceInsertion(...) 对红黑树进行调整
// 2.2.4 确保红黑树的根节点是哈希桶数组指定桶位上的第1个结点
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
putTreeVal(…) 同 putVal(…) 返回的是父结点或前继结点
插入树结点可以分为三部分的处理逻辑:
- kc :标识 key 对应的类是否可比较
根据执行kc = comparableClassFor(k)
的结果分两种情况考虑:
① kc != null :kc 可比较,则会执行compareComparables
若 k 和 pk 可通过重写的 compareTo(obj) 方法比较出 key 的大小,则以此作为 dir 的值;
若未比较出大小,会遍历子树查找是否有相同hash和key的结点,找不到则通过比identityHashCode(obj)
获得的hash作为 dir值
② kc == null :kc 不可比较,不可比较就不比较,也即不会通过执行compareComparables
比较 key,若是首次搜索子树,则搜索子树
- searched 标识是否搜索过子树,若搜索过子树则不再进行搜索
搜索子树返回条件:(ph == h) && ((pk = p.key) == k || (k != null && k.equals(pk)))
即在子树中找到 hash 和 key 均相同的结点
搜索子树要求 kc 必须是可比较的,且能通过 compareTo(obj) 方法比较出 key 的大小
则若 kc 不可比较,会比较 p.left 和 p.right 的 hash 值和 key 值,若还是无法找到,则返回 null,此时 dir = 0
搜索过子树仍然返回null,说明确实找不到,会根据原生的hashcode 设置 dir 然后找到p空闲的位置插入
- dir 执行
1,2
的目的都是为了获取 dir 或 返回旧值
① 返回 p :找到hash值相同且key相同的结点,直接返回
(ph == h) && ((pk = p.key) == k || (k != null && k.equals(pk)))
② 返回 q:通过搜索左右子节点找到返回,找到条件同①
总结:红黑树是根据 hash 大小进行插入,hash 冲突的时候会比较 key 是否相等,不相等时会判断是否可比较【compareTo(obj)】
不可比较:首次遍历时,搜索子树的左右子节点判断key和hash 是否相等,不相等则通过 原生 hash比较
可比较:首次遍历时,通过 compareComparables
中的compareTo(obj)
比较 key的大小
比较不出来会到子树中搜索hash和key相同的返回,若找不到则通过 原生 hash 比较
非首次遍历:当 K 可比较时,直接执行按空插入的逻辑;K不可比较时,比较原生 identityHashCode(obj)
经过最后一个 else if 分支出来时,要么返回在子树中查找到的旧值;
要么设置 dir 的值,然后执行按空插入的逻辑
上面的图有点问题:K 可比较,kc 被赋值后,不会再进入 kc==null
的分支,但是不知道怎么画了
6.3.1 balanceInsertion(arg…)
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// 1. 父节点为null,说明为根结点,涂黑并返回
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 2. 父节点为黑色不需要调整,或者父节点为红色但无父节点
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 3. 父结点为红色且父结点为左子树
if (xp == (xppl = xpp.left)) {
// 3.1 叔叔结点不为空且为红色
if ((xppr = xpp.right) != null && xppr.red) {
...
}
else {
// 3.2 叔叔结点为空或者为黑色,红父左,红子右,左旋父结点
if (x == xp.right) {
...
}
// 3.3 红色连续结点在同侧,旋转祖父结点[祖父结点不存在时变更旋转后原父节点的颜色]
if (xp != null) {
...
if(xpp != null){}
}
}
}
// 4. 父结点为红色且父结点为左子树
else {
// 以下内容同上 3.1, 3.2, 3.3
...
}
}
}
如下图所示,插入5
演示了上述父结点为红时且进入左分支的if
条件分支的执行过程
① 插入5
之前,父节点 xp = 4
,叔叔节点xppr = 10
,祖父结点 xpp = 7
② 父节点和叔叔结点均为红色,插入5
之后出现连续红色结点,将父节点和叔叔结点均涂红,同时将红色传递到祖父结点
if ((xppr = xpp.right) != null && xppr.red) {
// 1. 叔叔结点涂黑
xppr.red = false;
// 2. 父节点涂黑
xp.red = false;
// 3. 祖父结点涂红
xpp.red = true;
// 4. 回溯到祖父结点调整平衡,直到到达root
x = xpp;
}
③ 父结点为红色,叔叔结点为黑色
将 x 的引用执向 xpp 即以 xpp 为子结点继续向上调整
上一步 x = 5
,平衡后将红色传递给 xpp = 7
,设置 x = 7
继续调整
如下图所示:
x = 7
左子结点为红色,xp = 12
父结点为红色,xppr = 17
叔叔结点为黑色,满足大 if 中的 小else 分支条件
调整策略:将 xpp 结点右旋转,xp 升为父节点,xpp 降为右子节点
将 xpp 涂红,xp 涂黑
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
④ 父节点为红色,叔叔结点不存在
同上述 ③ 中的操作一样,满足左侧连续红色结点则将祖父结点右旋转并降级为右子树涂成红色
⑤ 连续红色不再同一侧时,即出现 LR 或者 RL 的情况时,现旋转父结点,使其调整到同一侧再转化为 ③或④
实际上:插入并调整叶子结点时,父节点必为红,若有叔叔结点则叔叔结点必为红
父节点为红但叔叔结点为黑的情况发生在红色结点属性向上传递时,需要调整被传递为红色的祖父结点使其平衡
下面一例:实属挖坑
分析:红色子节点和父结点不在同一侧的情况
代码执行流程:
- 父节点为红色左结点,子节点为红色右结点,先对父节左旋转从 ① -> ②
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
- 旋转后 x = xp 即当前调整结点由
3
变更到4
类似上述红色属性传递给祖父结点后对祖父结点调整
if (xp != null) {
xp.red = false;
if (xpp != null) {
...
}
}
此时 xp = 5
,xpp == null
,父节点设置为黑色得到 ②
- 连续红色出现在左侧,对祖父结点
5
进行右旋转
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
这里发现一个BUG,代码里面没有对5
进行颜色调整,直接返回4
?
实际上上述流程是错的,原因是当右结点不存在的时候,会退化成链表
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
6.3.2 moveRootToFront(arg…)
若经过调整后,root 不在哈希桶上,即可能出现旋转而使root变更
新root 经过运算后可能不在哈希桶上
这里即使是新root,也不会到其他桶上,
原因是:放在同一个桶上的前提是hash冲突,即经过位置运算(n - 1) & root.hash
的结果是一样的
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
// 1. root在 哈希桶上的位置
int index = (n - 1) & root.hash;
// 2. first 在哈希桶上的树结点
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
// 3. 若 root 不在哈希桶上
if (root != first) {
Node<K,V> rn;
// 3.1 设置哈希桶上的树结点为 root
tab[index] = root;
// 3.2 root 的前驱结点
TreeNode<K,V> rp = root.prev;
// 3.3 root 在双链表中的后继非null
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
// 3.4 root 的前驱和后继均非null,则连接两者,即移除链表中root当前的位置
if (rp != null)
rp.next = rn;
// 3.5 原哈希桶上结点非null,则连接双链表中 root和first
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
// 4. 检查双链表连接是否合法,红黑树是否满足hash值有序,红黑树是否出现连续红结点...
assert checkInvariants(root);
}
}
将 root 移至哈希桶上不会再涉及红黑树的调整,而是修改双链表中的连接,以及将哈希桶上的树结点设置为当前root
6.3.4 rotateLeft(arg…)
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
// 1. 原父结点p, p 左旋转, 新父结点为右孩子,p.right 即 r
if (p != null && (r = p.right) != null) {
// 2. 将右孩子的左分支连接到旧父结点的右分支上【右支大于父结点】
if ((rl = p.right = r.left) != null)
rl.parent = p;
// 3. 设置新父结点r的父节点为其原祖父结点
if ((pp = r.parent = p.parent) == null)
// 3.1 原祖父节点为null,即新根r为root,则将其涂黑
(root = r).red = false;
// 4. 原祖父结点非null,判断原父结点p在原祖父结点中位置,将新结点连接到该位置
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
// 5. 连接新父结点和旧父结点
r.left = p;
p.parent = r;
}
// 6. 返回根结点
return root;
}
6.3.5 rotateRight(arg…)
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
// 1. 原父结点 p, p右旋转,新父结点为左孩子,p.left 即 l
if (p != null && (l = p.left) != null) {
// 2. 将左孩子的右分子连接到旧父结点的左分支上
if ((lr = p.left = l.right) != null)
lr.parent = p;
// 3. 设置新父结点的父结点为其原祖父结点
if ((pp = l.parent = p.parent) == null)
// 3.1 若祖父结点为null,即新结点为根结点,将其设置为黑色
(root = l).red = false;
// 3.2 祖父结点不为null,判断原父结点在原祖父结点中位置,将新结点连接到该位置
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
// 4. 将原父结点连接到新父结点的右侧,并更新父节点
l.right = p;
p.parent = l;
}
// 5. 返回根结点
return root;
}
7. get()
通过 get() 方法返回 null,不代表不存在相应的映射,可能映射的value 值确实为 null,hashmap 允许一个null - key,而允许多个 null - value
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
若不存在key的映射或映射为null,则返回 null,否则返回 映射的值 e.value
7.1 getNode(int hash, Object key)
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 1. 比较首结点的hash和key判断是否为待查找的结点,是则直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 2. 判断首结点是否有后继结点,若无则返回 nulll
if ((e = first.next) != null) {
// 2.1 判断是否为 TreeNode 类型的结点
if (first instanceof TreeNode)
// 2.1.1 是,则调用 getTreeNode(hash, key)获取结点
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 2.2 不是 TreeNode 类型结点,则遍历链表继续查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 2.2.1 若找到 hash 和 key 相等的点则返回
return e;
} while ((e = e.next) != null);
}
}
// 3. 未找到返回 null
return null;
}
7.2 getTreeNode(int h, Object k)
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
查找流程:
① 定位到根节点 root
② 调用 find(h, k, null) 从 root 结点遍历比较 hash 和 key,找到不到则遍历子树
7.2.1 find(arg…)
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
// 1. 比较 hash 值是否相等
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
// 2. hash 值相等判断 key 是否相等,相等则返回 p
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 3. 若左子树为空,则到右子树中查找
else if (pl == null)
p = pr;
// 4. 若右子树为空则到左子树中查找
else if (pr == null)
p = pl;
// 5. 左右子树均非null,则若 k 可比较,通过比较 key 设置下一步搜索方向
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// 6. 在右子树中找到,则返回
else if ((q = pr.find(h, k, kc)) != null)
return q;
// 7. 继续遍历左子树
else
p = pl;
} while (p != null);
// 8. 左右子树均未找到,返回 null
return null;
}
8. remove()
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
matchValue :false
movable : true
8.1 removeNode(arg…)
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 1. 哈希桶当前位置有元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 2. 若首节点的 hash 和 key 与待删除结点相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 3. 首节点有后继结点
else if ((e = p.next) != null) {
// 3.1 若为 TreeNode 类型的结点,则调用 getTreeNode() 找到待删除的结点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 3.2 遍历链表,若找到hash和key与待查找结点相同则返回
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
// p 保留 e 的前驱结点
p = e;
} while ((e = e.next) != null);
}
}
// 4. 到此处,node 为 null 或待删除结点的引用,若为null则返回null
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 4.1 node 为 TreeNode 类型,则调用 removeTreeNode(this,tab,movable)
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 4.2 若删除链表上的首结点,即哈希桶上的结点,则设置哈希桶上的元素为当前结点的下一个结点
else if (node == p)
tab[index] = node.next;
// 4.3 若删除链表中间某结点,则连接该结点的前驱结点和后继结点
else
p.next = node.next;
// 4.4 修改操作使map结构变化,因此 modCount++
++modCount;
// 4.5 更新hashmap 元素数量
--size;
afterNodeRemoval(node);
// 4.7 返回删除的结点
return node;
}
}
// 4.8 待删除结点不存在,返回 null
return null;
}
8.2 removeTreeNode(arg…)
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
// 1. 若 tab 为 null,或哈希桶为空则返回
if (tab == null || (n = tab.length) == 0)
return;
// 2. 获取待删除结点在哈希表中的位置
int index = (n - 1) & hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
// 3. 删除根结点,则将root的后继放入哈希桶首元素位置
if (pred == null)
tab[index] = first = succ;
// 4. 删除中间结点,将双链表上该结点的前驱指向后继,即移除双链表上该结点的位置
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
// 5. 若待删除位置哈希桶为空,或者该结点仅有一个元素,则直接返回
if (first == null)
return;
// 6. 寻找根节点 root,此处的 root 指向了 first,若删除根结点后继结点放入哈希桶,需要找到后继结点的父结点
if (root.parent != null)
root = root.root();
// 7. 若root为null,或 root 的左右子树为null,或左子树的左孩子为null则退化为链表,在链表中删除
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
// 8. 寻找替代结点
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
// 8.1 p 的左右子节点均非null
TreeNode<K,V> s = pr, sl;
// 8.2 s 右子树的最小值
while ((sl = s.left) != null) // find successor
s = sl;
// 8.3 交换p右子树最小值与p结点的颜色
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
// 8.4 p 仅有一个右孩子,则交换两者的位置
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
// 8.5 获取 s 在父节点中的位置,交换 p 到该位置
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
// 8.6 获取 p 在 pp 中的位置,交换 s 到该位置
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
// 9. p 仅有一个孩子[红孩子]时,直接用该孩子代替父节点
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
// 10. 替代结点非 p,则找到 p 在p的父节点中的位置,然后替换,并从树节点中移除 p
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
// 11. 替代结点为 p 则找到 p 在p的父节点的位置,然后删除该位置上的结点
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
// movable = true, 将root移至双链表的表首,即哈希桶上的位置
if (movable)
moveRootToFront(tab, r);
}
基本思路:
① 若为叶子结点,或仅含有一个红孩子的父结点则直接返回,或用红孩子代替该点
② 含有两个孩子,则找到右子树的最左结点 s ,s 和 p 交换位置,若 最左结点有右孩子 sr,则 sr 与 p 交换位置
有点疑问:为什么不直接找左孩子的最右子树进行交换???
大概上述分析还有点问题