HashMap介绍
Map接口的基于哈希表的实现。 给所有的实现类提供一些基础方法,HashMap允许空值和空键。( HashMap类大致等同于Hashtable ,除了它是非同步的并且允许空值。)该类不保证映射的顺序; 特别是,它不保证订单会随着时间的推移保持不变。 此实现为基本操作( get和put )提供恒定时间性能,假设散列函数在存储桶中正确分散元素。 迭代集合视图需要的时间与HashMap实例的“容量”(桶的数量)加上它的大小(键值映射的数量)成正比。 因此,如果迭代性能很重要,则不要将初始容量设置得太高(或负载因子太低),这一点非常重要。 HashMap的实例有两个影响其性能的参数:初始容量和负载因子。 容量是哈希表中的桶数,初始容量就是哈希表创建时的容量。 负载因子是衡量哈希表在其容量自动增加之前允许达到多满的指标。 当哈希表中的条目数超过负载因子和当前容量的乘积时,重新哈希表(即重建内部数据结构),使哈希表具有大约两倍的桶数。 作为一般规则,默认负载因子 (.75) 在时间和空间成本之间提供了很好的权衡。 较高的值会减少空间开销,但会增加查找成本(反映在HashMap类的大多数操作中,包括get和put )。 在设置其初始容量时,应考虑映射中的预期条目数及其负载因子,以尽量减少重新哈希操作的次数。 如果初始容量大于最大条目数除以负载因子,则不会发生重新哈希操作。 如果要在一个HashMap实例中存储许多映射,则创建具有足够大容量的映射将允许更有效地存储映射,而不是让它根据需要执行自动重新散列以增加表。 请注意,使用具有相同hashCode()多个键是降低任何哈希表性能的可靠方法。 为了改善影响,当键是Comparable ,此类可以使用键之间的比较顺序来帮助打破联系。 请注意,此实现不是同步的。 如果多个线程并发访问一个散列映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步。 (结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键关联的值不是结构修改。)这通常是通过同步一些自然封装映射的对象来完成的. 如果不存在这样的对象,则应使用Collections.synchronizedMap方法“包装”地图。 这最好在创建时完成,以防止对地图的意外不同步访问: Map m = Collections.synchronizedMap(new HashMap(...)); 此类的所有“集合视图方法”返回的迭代器都是快速失败的:如果在迭代器创建后的任何时间对映射进行结构修改,除了通过迭代器自己的remove方法外,迭代器将抛出ConcurrentModificationException . 因此,面对并发修改,迭代器快速而干净地失败,而不是在未来不确定的时间冒着任意、非确定性行为的风险。 请注意,无法保证迭代器的快速失败行为,因为一般而言,在存在非同步并发修改的情况下不可能做出任何硬保证。 快速失败的迭代器会尽最大努力抛出ConcurrentModificationException 。 因此,编写一个依赖此异常来确保其正确性的程序是错误的:迭代器的快速失败行为应该仅用于检测错误。 此类是Java Collections Framework的成员。 因为: 1.2 请参见: Object.hashCode() 、 Collection 、 Map 、 TreeMap 、 Hashtable 类型参数: – 此映射维护的密钥类型 – 映射值的类型 < 1.8 >
Map接口基于Hash表的实现,HashMap实现了Map接口可以使用Map中提供的所有方法
package com.example.demo;
import java.util.HashMap;
/**
* @author: yangqiang
* @create: 2020-05-14 14:43
*/
public class TestMain {
public static void main(String[] args) {
HashMap<testClass, Object> map = new HashMap<>(8);
for(int i = 0;i<100;i++){
map.put(new testClass(1),i);
}
}
}
package com.example.demo;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* @author: yangqiang
* @create: 2020-05-18 17:38
*/
@Data
@AllArgsConstructor
public class testClass {
private int key;
@Override
public int hashCode() {
return 1;
}
}
首先对第一个put进行源码分析
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
/**中文翻译
*将指定值与此映射中的指定键相关联。
*如果映射先前包含密钥的映射,则
*值被替换。
*与指定值关联的@param密钥
*@param value要与指定键关联的值
*@返回与<tt>键相关的上一个值,或
*<tt>如果没有<tt>键的映射,则为空。
*(A<tt>null</tt>返回也可以指示映射
*以前与键关联的<tt>null</tt>)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
/**
*利用key的整个hashCode值和key的hashCode高十六位进行异或操作
* 为什么要右移16位?
*其实是为了减少碰撞,进一步降低hash冲突的几率。int类型的数值是4个字节的,右移16位异或可以同时保留高16位于低16位*的特征
*为什么要异或运算?
*首先将高16位无符号右移16位与低十六位做异或运算。如果不这样做,而是直接做&运算那么高十六位所代表的部分特征就可能
*被丢失 将高十六位无符号右移之后与低十六位做异或运算使得高十六位的特征与低十六位的特征进行了混合得到的新的数值中就*高位与低位的信息都被保留了 ,而在这里采用异或运算而不采用& ,| 运算的原因是 异或运算能更好的保留各部分的特征,如*果采用&运算计算出来的值会向1靠拢,采用|运算计算出来的值会向0靠拢
int a = 100000 转为二进制为 11000011010100000 补0到32位位 00000000000000011000011010100000
a>>16 = 1 右移16位后为 00000000000000000000000000000001
a^a>>16 00000000000000011000011010100000
00000000000000000000000000000001
00000000000000011000011010100001
异或操作相同为0 不同为1
这样可以很好的保留高16位的特性 降低hash冲突
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* Implements Map.put and related methods.
*
* @param hash hash for key 通过hash方法计算出来的hash值
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
10000
01111
&操作后000000
&与操作 都为1是为1 其他为0
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断整个数组是否为空 HashMap在未指定容量的情况下
//首次put会通过resize()方法创建一个长度为DEFAULT_INITIAL_CAPACITY=16的Node<K,V>[] table数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//假设为未扩容情况下 用 16-1 = 15 1111 和 hash 去进行与操作 得到的是一个 0-15之间的数字
//用这个数字下标去table查找 如果没有值就进行赋值
if ((p = tab[i = (n - 1) & hash]) == null)
//hash 为进行hash操作得到的hash值 key-value: put操作的key-value null:存储下一个节点的信息
tab[i] = newNode(hash, key, value, null);
else {
//如果通过这个数字下标查找table发现存在值则拿到table的第i个元素p
Node<K,V> e; K k;
//判断p的hash值和put操作的hash值是否相等,不一样的key也有可能存在相同的hash
//进一步判断p的key值和key是否相同 如果相同的话直接将这个Node覆盖掉原来的Node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//如果这个p是红黑树的话 。。。。。。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//一直往p的next下级进行查找,binCount表示下级层数
for (int binCount = 0; ; ++binCount) {
//如果p.next == null 表示 p的下一个元素是null 则直接把插入的这个Node放到p的next下
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;
}
//如果p的某个下级元素key和进行put操作的key值相同 则直接break e里面已经存储了旧的key值对应的 Node元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//一直往下层寻找 e = p.next 就相当于 把p.next 赋值给 p继续向下寻找
p = e;
}
}
// 如果e!=null 说明put操作存在了相同的key 值这个时候进行value值的替换,并return回原来的value值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//操作HashMap的次数 +1
++modCount;
//如果 这个HashMap中的元素数量超过了 容量 * 负载因子, 未指定时时16 * 0.75 时进行扩容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
//如果到了这一步说明put操作没有相同的key值则返回null
return null;
}
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
* 扩容或者初始化操作
*/
final Node<K,V>[] resize() {
// transient Node<K,V>[] table;
// 第一次put的时候 table是null oldCap = 0 threshold = 0
//
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//如果oldCap>0 说明是进行扩容操作
if (oldCap > 0) {
// static final int MAXIMUM_CAPACITY = 1 << 30;
// 如果oldCap数组容量 >=MAXIMUM_CAPACITY 这个值时 HashMap将不再进行扩容操作
if (oldCap >= MAXIMUM_CAPACITY) {
//将扩容阈值提高到Integer.MAX_VALUE;2147483647
threshold = Integer.MAX_VALUE;
return oldTab;
}
//数组长度✖️2 再和MAXIMUM_CAPACITY 比较
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// double threshold threshold ✖️ 2
newThr = oldThr << 1;
}
else if (oldThr > 0)
// initial capacity was placed in threshold
// 初始容量等于扩容阈值 阈值会自动向上取 2 的次方数
newCap = oldThr;
else {
// zero initial threshold signifies using defaults
// 第一次操作HashMap这时候map还是空的
// static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 设置容量为默认值 16
// 阈值为 容量 * 负载因子
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数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//把数组赋值给全局变量 table
table = newTab;
//如果oldTab不为null的话就要将原来的table赋值到新数组中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果e没有next后 把这个元素按照e.hash & (newCap - 1)来存储到新的下标位置
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 { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//数组扩容为两倍后e.hash & (newCap - 1)和原来的得到的就不一样了
//(e.hash & oldCap) == 0 用来判断哪些是还存在原来的下标下的元素
//(e.hash & oldCap) == 0
// 假设oldCap为16 10000 (e.hash & oldCap) == 0的话 则 e.hash的二进制数第五位肯定为0
// e.hash 对 31做逻辑与操作的话 肯定是0-15之间的一个值 所以还放在原来的数组下标下
// (e.hash & oldCap) == 0的话 则 e.hash的二进制数第五位肯定为1
// e.hash 对 31做逻辑与操作的话 肯定是16-31之间的一个值 所以还放在原来的数组下标 +oldCap
//hashMap用这种方式完成 扩容后元素的放置位置操作
//https://blog.csdn.net/u010425839/article/details/106620440/
//可推导出满足e.hash&oldCap不等于0的元素,其在新数组中的索引位置是其在旧数组中索引位置的基础上再加 //上旧数组长度个偏移量
//(e.hash & oldCap) == oldCap 的话判断哪些元素存在于扩容后的下标
if ((e.hash & oldCap) == 0) {
if (loTail == null)
//如果loTail 是第一次进来拿到的是一条链表的第一个元素
//直接确定链表的第一个元素
loHead = e;
else
//第二个以后的元素进来后则改变loTail 的next值来进行链表的赋值
loTail.next = e;
//每次将进来的Node数据进行赋值给loTail 用于loTail给next进行赋值操作
loTail = e;
}
else {
//(e.hash & oldCap) == oldCap 的话判断哪些元素存在于扩容后的下标
//和上面的操作一样区别是在 整个table数组中第j个链表
//区分出那些Node元素属于当前这个j下标,那些属于 j +oldCap 下标 再进行赋值
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
//把最后一个Node元素的next置为null
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
//把最后一个Node元素的next置为null
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新生成的链表数组
return newTab;
}
第一次put操作主要是进行了Node数组的创建再找到hash对应的数组下标 进行数组的赋值,第二次put操作由于我们对hash做了特殊处理所以每次的out操作对应的下标都是一样的,直接到第8次操作,这时候链表的长度为8进行链表转为红黑树操作(在转为红黑树操作之前会判断数组长度是否大于64再进行转为树操作,小于64则进行扩容操作)
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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)
//如果Node数组的当前这个节点已经转为了红黑树结构。那接下来的put操作就需要往红黑树增加叶子节点了。
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
//这时候链表的长度为9了next里头一共有8个Node元素进行链表转为红黑树操作
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;
}
TreeNode继承自LinkedHashMap内部类Entry<K,V>,而Entry<K,V>又继承自HashMap.Node<K,V>
/**
* 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;
// static final int MIN_TREEIFY_CAPACITY = 64; 为什么是64我也不知道
// 数组长度如果小于64则进行一次扩容操作,可以将链表的长度进行缩小
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//hash为该条链表下的最后一个Node元素
//e =tab[index = (n - 1) & hash] 获取该链表上第一个元素,即为该数组存储的这个下标的元素
//定义首尾节点hd,tl
TreeNode<K,V> hd = null, tl = null;
do {
//将该节点替换成红黑树节点
TreeNode<K,V> p = replacementTreeNode(e, null);
// 如果尾节点为空,说明还没有根节点
if (tl == null)
//把当前节点设置为首节点
hd = p;
else {
//第二次以后的转化操作时 这时已经有首尾节点了需要做的是把两个节点串联起来形成双向链表
//把这个节点的prev 即为上一个节点设置为尾结点
p.prev = tl;
//把尾节点的next 即为下一个节点设置为当前节点
tl.next = p;
}
//把当前节点设置为尾结点方便下一次操作
tl = p;
} while ((e = e.next) != null);
//到这里的hd已经是一个双向链表了
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
// For treeifyBin
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
红黑树
特性
-
每个节点要么是黑色,要么是红色。(节点非黑即红)
-
根节点是黑色。
-
每个叶子节点(NIL)是黑色。
-
如果一个节点是红色的,则它的子节点必须是黑色的。(也就是说父子节点不能同时为红色)
-
从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。(这一点是平衡的关键)
(红黑树并不是一个完美平衡二叉查找树,从图1可以看到,根结点P的左子树显然比右子树高,但左子树和右子树的黑结点的层数是相等的,也即任意一个结点到到每个叶子结点的路径都包含数量相同的黑结点(性质5)。所以我们叫红黑树这种平衡为黑色完美平衡)
-

/** * Forms tree of the nodes linked from this node. * 将链表转换成红黑树 */final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null; for (TreeNode<K,V> x = this, next; x != null; x = next) { //遍历链表中的每一个TreeNode,当前结点为x next = (TreeNode<K,V>)x.next; x.left = x.right = null; if (root == null) { //第一个节点进来的时候设置为root节点 设置为黑色 x.parent = null; x.red = false; root = x; } else { //余下的节点 K k = x.key; int h = x.hash; Class<?> kc = null; //从根结点开始遍历,寻找当前结点x的插入位置 for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; //如果当前结点的hash值小于根结点的hash值,方向dir = -1; if ((ph = p.hash) > h) dir = -1; else if (ph < h) //如果当前结点的hash值大于根结点的hash值,方向dir = 1; dir = 1; else if ((kc == null && //如果x结点的key没有实现comparable接口,或者其key和根结点的key相等 //(k.compareTo(x) == 0)仲裁插入规则 (kc = comparableClassFor(k)) == null) || //只有k的类型K直接实现了Comparable<K>接口,才返回K的class,否则返回null,间接实现也不行。 (dir = compareComparables(kc, k, pk)) == 0) //仲裁插入规则 dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; //判断 p的左右节点是否存在不存在则进行插入。存在则继续循环 if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; //dir <= 0,插入到左儿子 if (dir <= 0) xp.left = x; else //dir <= 0,插入到右儿子 xp.right = x; //插入后进行树的调整,使之符合红黑树的性质 root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root);} /** * Tie-breaking utility for ordering insertions when equal * hashCodes and non-comparable. We don't require a total * order, just a consistent insertion rule to maintain * equivalence across rebalancings. Tie-breaking further than * necessary simplifies testing a bit. * System.identityHashCode() 返回的是 Object.hashcode() 而不是重写类的x.hashcode() */ static int tieBreakOrder(Object a, Object b) { int d; if (a == null || b == null || (d = a.getClass().getName(). compareTo(b.getClass().getName())) == 0) d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1); return d; }
/** * Returns the same hash code for the given object as * would be returned by the default method hashCode(), * whether or not the given object's class overrides * hashCode(). * The hash code for the null reference is zero. * * @param x object for which the hashCode is to be calculated * @return the hashCode * @since JDK1.1 * 返回与给定对象相同的哈希代码 * 将由默认方法hashCode()返回, * 给定对象的类是否重写 * 哈希代码()。 * 空引用的哈希代码为零。 * @要为其计算哈希代码的param x对象 * @返回哈希码 * @从JDK1.1开始 */public static native int identityHashCode(Object x);
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) { //当前要插入树的节点 先定义为红色节点 因为红黑树插入红色节点的话对 红黑树的第五条定义不影响 //对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数码的黑节点 x.red = true; //xp x.parent,xpp xp.parent,xppl xp.parent.left,xppr xp.parent.right for (TreeNode<K,V> xp, xpp, xppl, xppr;;) { //第一次root节点进来时的操作 根节点设置为 黑色 思维导图 1 if ((xp = x.parent) == null) { x.red = false; return x; } //第二次以后的节点进入 如果xp为黑色 或者 xp是根节点 xpp为 null 那当前节点设置为红色 else if (!xp.red || (xpp = xp.parent) == null) return root; //如果当前节点的parent节点为左子节点 if (xp == (xppl = xpp.left)) { //当前节点的parent.parent右子节点不为空并且右子节点为红色 if ((xppr = xpp.right) != null && xppr.red) { //当前节点的parent 和 当前节点的parent.parent右子节点(叔节点)设置为黑色 xppr.red = false; xp.red = false; //当前节点的parent.parent设置为红色 xpp.red = true; x = xpp; } else { //思维导图3的结构 第三个Node节点插进来 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); } } } } }}
红黑树插入数据场景
插入情景1:红黑树为空树
最简单的一种情景,直接把插入结点作为根结点就行,但注意,根据红黑树性质2:根节点是黑色。还需要把插入结点设为黑色。
处理:把插入结点作为根结点,并把结点设置为黑色。
插入情景2:插入结点的Key已存在
插入结点的Key已存在,既然红黑树总保持平衡,在插入前红黑树已经是平衡的,那么把插入结点设置为将要替代结点的颜色,再把结点的值更新就完成插入。
处理:
- 把I设为当前结点的颜色
- 更新当前结点的值为插入结点的值
插入情景3:插入结点的父结点为黑结点
由于插入的结点是红色的,当插入结点的黑色时,并不会影响红黑树的平衡,直接插入即可,无需做自平衡。
处理:直接插入。
插入情景4:插入结点的父结点为红结点
-
父节点的父节点的右子节点(叔叔节点) 为红色:
第一步:变色操作 父节点和叔叔节点变为黑色节点 祖父节点变为红色
第二步:把祖父节点当成是插入的节点 重复这8中场景进行操作
-
父节点的父节点的右子节点(叔叔节点) 不存在或者为黑色:
-
父节点是祖父节点的左子节点
右旋:当前节点父节点改为 祖父节点的父节点 当前节点的右子节点改为原祖父节点的左子节点,当前节点的左子节点不变
-
父节点是祖父节点的右子节点
左旋:当前节点父节点改为 祖父节点的父节点 当前节点的左子节点改为原祖父节点的右子节点,当前节点的左子节点不变
-