HashMap源码学习
jdk 1.7 之前,HashMap底层是由数组+链表
组成
jdk 1.8 之后,HashMap引入红黑树,底层由数组+链表+红黑树
组成
静态成员变量
/**
* 默认的初始容量:16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 树化阈值。当添加元素到桶中时,如果桶中链表长度被添加到至少8,链表就转换为红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 链表还原阈值。当减少桶中的元素时,如果桶中红黑树的节点个数减少到至多6,红黑树就转化为链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 如果桶数量小于64,直接扩容而不用树化,因为扩容之后,链表会分化成两个链表,达到减少元素的作用
*/
static final int MIN_TREEIFY_CAPACITY = 64;
内部链表节点类
-
HashMap通过拉链法解决冲突,当链表达到一定长度再转换为红黑树,下面是链表节点类
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; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value);//^表示相同返回0,不同返回1、异或 } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
非静态成员变量
/**
* 数组中存放指向链表的引用
* 哈希表,在第一次使用时初始化,当需要扩容时自动扩容。长度总是2的整数次幂
*/
transient Node<K, V>[] table;
transient Set<Map.Entry<K, V>> entrySet;
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
/**
* 已对HashMap进行结构修改的次数
* 结构修改是指更改HashMap中映射数量或以其他方式修改其内部结构(例如重新哈希)的修改。
*/
transient int modCount;
/**
* 要调整大小的下一个大小值(容量*负载系数)扩充阈值
* 通过容量*加载因子计算得到,当键值对的个数大于这个值就进行扩容。
* 这样做是为了减少哈希冲突。
* 浪费了一定的空间,但换来的是查找效率的提高。
*/
int threshold;
/**
* 哈希表的负载因子
*/
final float loadFactor;
构造函数
/**
* 带初始容量和加载因子的构造器
* 初始化时并没有创建数组,只是为loadFactor和threshold赋值,threshold记录了要初始化的容量
* @param initialCapacity 初始容量
* @param 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);
}
/**
* 指定的初始容量和默认的负载系数(0.75)
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 使用默认的初始容量(16)和默认的加载因子(0.75)构造一个空的HashMap 。
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* 构造一个新的HashMap,其映射与指定的Map相同。HashMap是使用默认负载因子(0.75)和足以将映射保存在指定的Map中的初始容量创建的。
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
//保证足够容量存放map中的元素且保留一定余量
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
//浪费一定空间、提升查询效率、减少哈希冲突
//求出大于等于t的最小的2的幂次
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
/**
* 保证返回值是2的幂次或者是默认最大元素数
* 构造函数中调用来设置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;
}
final HashMap.Node<K, V>[] resize() {}
hash函数(扰动函数)
//hash方法,hashCode的低十六位和高十六位异或,key为null时hash值为0
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- jdk8让hashcode的高16和低16位都参与到hash值的计算,是为了在哈希表长度较小的情况下,也能尽量的减少哈希冲突。将key的hashcode的低16位和高16位进行异或。
插入
-
当 table 数组为空时,通过扩容的方式初始化 table
-
通过计算键的 hash 值求出下标后,若该位置上没有元素(没有发生 hash 冲突),则新建 Node 节点插入
-
若发生了 hash 冲突,遍历链表查找要插入的 key 是否已经存在,存在的话根据条件判断是否用新值替换旧值
-
如果不存在,则将元素插入链表尾部,并根据链表长度判断是否将链表转为红黑树
-
判断键值对数量是否大于阈值,大于的话则进行扩容
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * onlyIfAbsent 如果是true,不改变已经存在的key的value值 * @return 如果节点已经存在,返回之前的value,否则返回null */ 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.当 table 数组为空时,通过扩容的方式初始化 table if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) //2.通过计算键的 hash 值求出下标后,若该位置上没有元素(没有发生 hash 冲突),则新建 Node 节点插入 tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //3.若发生了 hash 冲突,遍历链表查找要插入的 key 是否已经存在,存在的话根据条件判断是否用新值替换旧值 //判断首节点和要插入节点是否相等,首先判断hash值是否相同,再用equals()判断 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 { //4.如果不存在,则将元素插入链表尾部,并根据链表长度决定是否将链表转为红黑树 //是链表,遍历链表尾插法插入 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //如果链表长度大于等于8,树形化 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; } } //如果存在key值,当onlyIfAbsent==true时,不更新value值,否则更新value值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //5.判断键值对数量是否大于阈值,大于的话则进行扩容操作 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
get
-
将 key hash 之后取得所定位的桶。
-
如果桶为空则直接返回 null 。
-
否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
-
如果第一个不匹配,则判断它的下一个是红黑树还是链表。
-
红黑树就按照树的查找方式返回值。
-
不然就按照链表的方式遍历匹配返回值。
public V get(Object key) {
Node<K,V> e;
//1.将 key hash 之后取得所定位的桶。。
return (e = getNode(hash(key), key)) == null ? null : e.value;//hash(key)不等于key.hashCode
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; //指向hash数组
Node<K,V> first, e; //first指向hash数组链接的第一个节点,e指向下一个节点
int n;//hash数组长度
K k;
//先判断,只有当哈希表不为空,且表的长度大于0,且key对应的位置有节点时,再进行操作,否则直接返回null
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
//先判断第一个节点与key是否匹配,如果匹配,返回第一个节点
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//如果是树节点,进行红黑树的查找过程
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {//否则遍历链表查找、判断节点和key是否匹配
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
扩容
调用resize的两种情况
- 第一次调用 HashMap 的 put 方法时,会调用 resize 方法对 table 数组进行初始化,如果不传入指定值,默认大小为 16。
- 扩容时会调用 resize,即 size > threshold 时,table 数组大小翻倍。
resize方法的逻辑
HashMap 每次扩容都是建立一个新的 table 数组,长度和容量阈值都变为原来的两倍,然后把原数组元素重新映射到新数组上,具体步骤如下:
-
首先会判断 table 数组长度,如果大于 0 说明已被初始化过,那么按当前 table 数组长度的 2 倍进行扩容,阈值也变为原来的 2 倍
-
若 table 数组未被初始化过,且 threshold(阈值)大于 0 说明调用了 HashMap(initialCapacity, loadFactor) 构造方法,那么就把数组大小设为 threshold
-
若 table 数组未被初始化,且 threshold 为 0 说明调用 HashMap() 构造方法,那么就把数组大小设为 16,threshold 设为 16*0.75
-
接着需要判断如果不是第一次初始化,那么扩容之后,要重新计算键值对的位置,并把它们移动到合适的位置上去,如果节点是红黑树类型的话则需要进行红黑树的拆分。
这里有一个需要注意的点就是在 JDK1.8 HashMap 扩容阶段重新映射元素时不需要像 1.7 版本那样重新去一个个计算元素的 hash 值,而是通过 hash & oldCap 的值来判断,若为 0 则索引位置不变,不为 0 则新索引=原索引+旧数组长度,为什么呢?具体原因如下:
因为我们使用的是 2 次幂的扩展(指长度扩为原来 2 倍),所以,元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置。因此,我们在扩充 HashMap 的时候,不需要像 JDK1.7 的实现那样重新计算 hash,只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成“原索引 +oldCap
/*扩容*/
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、若oldCap>0 说明hash数组table已被初始化
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//按当前table数组长度的2倍进行扩容,阈值也变为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
//2、若数组未被初始化,而threshold>0说明调用了HashMap(initialCapacity)和HashMap(initialCapacity, loadFactor)构造器
else if (oldThr > 0)
newCap = oldThr;//新容量设定为数组阈值
else {
//3、若table数组未被初始化,且threshold为0说明调用HashMap()构造方法
newCap = DEFAULT_INITIAL_CAPACITY;//默认为16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//16*0.75
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//创建新的hash数组,hash数组的初始化也是在这里完成的
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果旧的hash数组不为空,则遍历旧数组并映射到新的hash数组
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 {
// 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash
//尾插法
//rehash————>重新映射到新数组
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,若为0则索引位置不变,不为0则新索引=原索引+旧数组长度*/
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;
}
删除
- 定位桶位置
- 遍历链表找到相等的节点
- 第三步删除节点
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
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;
// 如果键的值与链表第一个节点相等,则将 node 指向该节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 如果是 TreeNode 类型,调用红黑树的查找逻辑定位待删除节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 2、遍历链表,找到待删除节点
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 3、删除节点,并修复链表或红黑树
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
- 注意:删除节点后可能破坏了红黑树的平衡性质,removeTreeNode 方法会对红黑树进行变色、旋转等操作来保持红黑树的平衡结构,这部分比较复杂。
遍历
Iterator<Map.Entry<String, Integer>> entryIterator = map.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<String, Integer> next = entryIterator.next();
System.out.println("key=" + next.getKey() + " value=" + next.getValue());
}
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()){
String key = iterator.next();
System.out.println("key=" + key + " value=" + map.get(key));
}
-
建议使用第一种EntrySet方法进行遍历
第一种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低。
红黑树
红黑树定义和性质
红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:
- 性质1:每个节点要么是黑色,要么是红色。
- 性质2:根节点是黑色。
- 性质3:每个叶子节点(NIL)是黑色。
- 性质4:每个红色结点的两个子结点一定都是黑色。
- 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
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指向前驱节点的指针,与next指针相反,删除后需要取消next指针的链接
boolean red; //节点颜色
TreeNode(int hash, K key, V val, Node<K,V> next) {//构造函数,指向HashMap的Node类的构造函数
super(hash, key, val, next);
}
}
- TreeNode 继承了 LinkiedHashMap.Entry,LinkedHashMap.Entry继承了 HashMap.Entry
内部方法
左旋
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
//r:p的right节点,pp:p的parent节点,rl:p的右孩子的左孩子节点
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
// “=”是右结合的。rl=p.right=r.left 相当于 rl=(p.right=r.left)
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
右旋
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
红黑树插入节点
插入操作包括两部分工作:一查找插入的位置;二插入后自平衡。查找插入的父节点很简单,跟查找操作区别不大:
- 从根节点开始查找;
- 若根节点为空,那么插入节点作为根节点,结束。
- 若根节点不为空,那么把根节点作为当前节点;
- 若当前节点为null,返回当前节点的父节点,结束。
- 若当前节点key等于查找key,那么该key所在节点就是插入节点,更新节点的值,结束。
- 若当前节点key大于查找key,把当前节点的左子节点设置为当前节点,重复步骤4;
- 若当前节点key小于查找key,把当前节点的右子节点设置为当前节点,重复步骤4;
- 插入后,父节点和叔叔节点是红色,爷爷节点是黑色:将爷爷节点变为红色,父节点和叔叔节点变为黑色,然后将爷爷节点作为当前节点继续比较
- 插入后,父节点是红色,叔叔节点是黑色或者Nil:
- LL双红:交换父节点和爷爷节点的颜色,右旋
- RR双红:交换父节点和爷爷节点的颜色,左旋
- LR双红:先左旋变为LL双红,转1
- RL双红:先右旋变为RR双红,转2
/**
* Tree version of putVal.
*/
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;
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
//HashMap的红黑树默认情况下根据节点的key的hash值排序,先比较当前节点的hash值和要插入节点的hash值的大小。如果大于,令dir=-1
if ((ph = p.hash) > h)
dir = -1;
//如果小于,令dir=1
else if (ph < h)
dir = 1;
//要插入的节点已经存在,返回存在的节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果当前插入的类型和正在比较的节点的Key是Comparable的话,就直接通过此接口比较
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
//尝试在p的左子树或者右子树中找到了目标元素
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);
}
TreeNode<K,V> xp = p;
//上面的所有if-else判断都是在判断下一次进行遍历的方向,即dir
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//当p为null,说明节点要插入在这个位置
Node<K,V> xpn = xp.next;
//新建x节点
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//根据dir就可以知道节点插入位置
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//因为红黑树可能会重新转换为链表,所以我们需要维护节点的next指针
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//结点插入完要进行红黑树的调整,并调整桶中结点保证桶中的第一个结点是根结点
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
插入后平衡操作
- 红黑树在插入一个节点后,需要保持红黑树的属性,需要进行平衡操作。
/**
* 插入节点的平衡操作
* @param root
* @param x
* @param <K>
* @param <V>
* @return
*/
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;;) {
//如果x没有父节点,说明x是根节点,设置x的颜色是黑色,然后返回x,如图1
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//如果父节点是黑色的,或者父节点的父节点是null(父节点是根节点),直接返回根节点,
else if (!xp.red || (xpp = xp.parent) == null)
return root;
//x节点在父节点的父节点的左子树上时
if (xp == (xppl = xpp.left)) {
//xp有兄弟节点且兄弟节点也是红色,进行颜色翻转操作
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
//跟上面的对称
else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
红黑树查找
因为红黑树是一颗二叉平衡树,并且查找不会破坏树的平衡,所以查找跟二叉平衡树的查找无异:
- 从根节点开始查找,把根节点设置为当前节点;
- 若当前节点为空,返回null;
- 若当前节点不为空,用当前节点的key跟查找key作比较;
- 若当前节点key等于查找key,那么该key就是查找目标,返回当前节点;
- 若当前节点key大于查找key,把当前节点的左子节点设置为当前节点,重复步骤2;
- 若当前节点key小于查找key,把当前节点的右子节点设置为当前节点,重复步骤2;
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;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
红黑树删除
删除过程与二叉查找树一样,分为三种情况
<1>无左右孩子、<2>有左孩子或右孩子、<3>既有左孩子又有右孩子
违反性质的三种情况:
1:若删除的是根节点,唯一子节点为红色,则根节点变成红色,违反了性质2.
2:若被删节点的唯一孩子和父亲都是红色,性质4被破坏.
3:被删节点会导致包含该点的路径的黑节点树-1,违反了性质5.
额外一重黑色:
顶替被删节点的节点还有一重额外的黑色,此时可能违背了性质1(红+黑),但在这种假设下,性质5成立。节点额外的黑色是针对X节点的,不是反映在color属性上的。
函数修复性质1,2,4:
性质2、4很简单,因为X为红色,所以将X变为黑色即可。
性质1:函数的目的是将额外的黑色沿树上移,直到:
X指向红黑节点,将x着为黑色。
X指向根节点。
执行适当的旋转和重新着色,退出循环。
注意
X总是指向具有双重黑色的非根节点。
w不可能是T.nil
性质5交换前后不变
x是左子节点
四种情况:
情况1:x的兄弟w是红色的
因为x是双重黑色,所以w必有黑色孩子。此时将w着为黑色,父亲着为红色,对父亲做一次左旋。此时x的新兄弟w'是左旋前w的某个子节点,颜色是黑色,这样将情况1转换为情况2、3或4。
情况2:x的兄弟w是黑色的,而且w的两个孩子都是黑色的。
因为x是双重黑色,且w和w的子节点都是黑色,所以从x和w上去掉一重黑色,即x只有一重黑色而w着为红色,给x的父节点添加额外黑色,不违反红黑树的任何性质
情况3:x的兄弟w是黑色的,w的左孩子是红色的,右孩子是黑色的
交换w和其左孩子的颜色,并对w进行右旋转而不违反红黑树的任何性质。旋转后x的新兄弟w'是一个有红色右孩子的黑节点,转换成了情况4。
情况4:x的兄弟w是黑色的,而且w的右孩子是红色的。
执行过程是将w的颜色设置为父亲的颜色,将父亲的颜色设置为黑色,将w的右孩子着为黑色,然后在父亲做一次左旋,最后将x设置为根root。
x是右子节点
四种情况:
情况1:x的兄弟w是红色的
因为x是双重黑色,所以w必有黑色孩子。此时将w着为黑色,父亲着为红色,对父亲做一次右旋。此时x的新兄弟w'是右旋前w的某个子节点,颜色是黑色,这样将情况1转换为情况2、3或4。
情况2:x的兄弟w是黑色的,而且w的两个孩子都是黑色的。
因为x是双重黑色,且w和w的子节点都是黑色,所以从x和w上去掉一重黑色,即x只有一重黑色而w着为红色,给x的父节点添加额外黑色,不违反红黑树的任何性质
情况3:x的兄弟w是黑色的,w的左孩子是红色的,右孩子是黑色的
交换w和其左孩子的颜色,并对w进行左旋转而不违反红黑树的任何性质。旋转后x的新兄弟w'是一个有红色右孩子的黑节点,转换成了情况4。
情况4:x的兄弟w是黑色的,而且w的右孩子是红色的。
执行过程是将w的颜色设置为父亲的颜色,将父亲的颜色设置为黑色,将w的右孩子着为黑色,然后在父亲做一次右旋,最后将x设置为根root。
-
默认判断链表第一个元素是否是要删除的元素;
-
如果第一个不是,就继续判断当前冲突链表是否是红黑树,如果是,就进入红黑树里面去找;
-
如果当前冲突链表不是红黑树,就直接在链表中循环判断,直到找到为止;
-
将找到的节点,删除掉,如果是红黑树结构,会进行颜色转换、左旋、右旋调整,直到满足红黑树特性为止;
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
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;
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
if (root.parent != null)
root = root.root();
if (root == null
|| (movable
&& (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
tab[index] = first.untreeify(map); // too small
return;
}
//p是待删除节点,s是替换节点,replacement用于后续的红黑树调整,指向的是p或者p的后继。
//如果p是叶子节点,p==replacement,否则replacement为p的右子树中最左节点
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
//p既有左孩子又有右孩子
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
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;
//p是s的父节点,s是p的左孩子节点
if (s == pr) { // p was s's direct parent
//交换s和p
p.parent = s;
s.right = p;
}
//s是p的右子树中的最左节点
else {
//交换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;
}
//交换s和p后s的右子节点和p的左子节点和父节点的调整
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;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
//p只有左孩子
else if (pl != null)
replacement = pl;
//p只有右孩子
else if (pr != null)
replacement = pr;
else
//p是叶子节点,既没有左孩子,也没有右孩子
replacement = p;
//以上是寻找删除节点和删除节点的替换节点,下面是删除操作。
if (replacement != p) {
//若p不是叶子节点,则让replacement的父节点指向p的父节点
TreeNode<K,V> pp = replacement.parent = p.parent;
//p只有右子节点
if (pp == null)
root = replacement;
//p是pp的左子节点
else if (p == pp.left)
pp.left = replacement;
//p是pp的右子节点
else
pp.right = replacement;
//释放p节点
p.left = p.right = p.parent = null;
}
//若待删除的节点p时红色的,则树平衡未被破坏,无需进行调整。
//否则删除节点后需要进行调整
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
//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;
}
}
//确保红黑树的根节点是桶中的第一个节点
if (movable)
moveRootToFront(tab, r);
}
删除后平衡
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
TreeNode<K,V> x) {
for (TreeNode<K,V> xp, xpl, xpr;;) {
//x为空或x为根节点,直接返回
if (x == null || x == root)
return root;
//x为根节点,染成黑色,直接返回(因为调整过后,root并不一定指向删除操作过后的根节点,如果之前删除的是root节点,则x将成为新的根节点)
else if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//如果x为红色,则无需调整,返回
else if (x.red) {
x.red = false;
return root;
}
//x为其父节点的左孩子
else if ((xpl = xp.left) == x) {
//如果它有红色的兄弟节点xpr,那么它的父亲节点xp一定是黑色节点,对应情况1
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
//对父节点xp做左旋转
root = rotateLeft(root, xp);
//重新将xp指向x的父节点,xpr指向xp新的右孩子
xpr = (xp = x.parent) == null ? null : xp.right;
}
//如果xpr为空,则向上继续调整,将x的父节点xp作为新的x继续循环
if (xpr == null)
x = xp;
else {
//sl和sr分别为其近侄子和远侄子,对应情况2
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
//若sl和sr都为黑色或者不存在,即xpr没有红色孩子,则将xpr染红
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
xpr.red = true;
x = xp;//本轮结束,继续向上循环
}
//否则有一个子节点==null或是黑色
else {
//右侄子节点==null或是黑色
if (sr == null || !sr.red) {
//左侄子节点!=null,对应情况3
if (sl != null)
sl.red = false;//将左侄子节点染黑
xpr.red = true;//兄弟节点染红
root = rotateRight(root, xpr);//对兄弟节点右旋
xpr = (xp = x.parent) == null ?
null : xp.right;//右旋后,xpr指向xp的新右孩子,即上一步中的sl
}
//兄弟节点!=null,对应情况4
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;//x!=null,兄弟节点!=null,xp可以==null?
//右侄子节点!=null
if ((sr = xpr.right) != null)
sr.red = false;//右侄子节点染黑,防止出现两个红色相连
}
if (xp != null) {
//将xp染黑,并对其左旋,这样就能保证被删除的X所在的路径又多了一个黑色节点,从而达到恢复平衡的目的
xp.red = false;
root = rotateLeft(root, xp);
}
//到此调整已经完毕,进入下一次循环后将直接退出
x = root;
}
}
}
//x为其父节点的右孩子,跟上面类似
else { // symmetric
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}
红黑树分裂
- 当链表长度大于等于树化阈值8时,链表结构会转换为红黑数结构存储冲突元素,在扩容时要进行树的分裂操作,如果当前索引中元素结构是红黑树且元素个数小于等于链表还原阈值6时,就会还原为链表结构。
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
//重新将节点连接到lo或hi,保持顺序
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
// 高位低位的树节点初始节点个数都是0
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;
// 节点的hash值和旧容量相与,如果是0,连接在低位树lo,低位树节点个数+1,如果是1,连接在高位树hi,高位树节点个数+1,
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) {
//如果数量<=UNTREEIFY_THRESHOL(6),将树转换为链表
if (lc <= UNTREEIFY_THRESHOLD)
//低位树放在index位置
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
//如果hiHead==null,说明loHead是原来的树,已经树形化了,就不用再树形化,否在要对loHead进行树形化
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
//高位树是放在(index+原来容量)的位置
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
链表转换为树
/**
* Forms tree of the nodes linked from this node.
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
//从this节点开始,this节点是桶中的第一个节点
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
//先对x节点初始化
x.left = x.right = null;
//如果根节点是null,那么x就是根节点
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
//否则,按照类似红黑树的插入操作进行转换
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
//上面判断了下一次便利的位置,即dir
TreeNode<K,V> xp = p;
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;
}
}
}
}
//确保红黑树的根节点是桶中的第一个节点
moveRootToFront(tab, root);
}
- 先判断表的容量是否达到了最小树形化容量64,如果没有,进行扩容操作。否则,将要树形化的桶中链表节点
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果表的容量小于 MIN_TREEIFY_CAPACITY,不树形化,进行扩容
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;
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);
//调用第一个节点的treeify方法
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
树转换为链表
/**
* Returns a list of non-TreeNodes replacing those linked from
* this node.
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
//从一个树节点得到一个链表节点
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}