HashMap学习
一、相关问题
1、什么是hash,hash有哪些特点
概念:在计算机科学中,hash是一种函数,该函数可以把任意长度的输入转换为固定长度的输出,输出的结果称为hash值,
通常为一个字符串,对于特定的hash算法输出结果的长度总是一定的。
特点:
1)对于特定的hash算法输出结果的长度总是一定的;
2)对于相同的输入内容不管执行多少次hash函数,总会得到相同的hash值;
3)对于输入字符串哪怕及其微小的变动也会引起输出结果巨大的变化;
4)哈希过程是单向的,即不能通过特定的函数从hash值恢复出原始数据。
2、为什么重写equals方法时,一定要重写hashCode
之所以重写hashCode,是从容器存储元素的角度考虑的。
通常情况下,一个容器需要快速判断的元素是否存在时,如果使用数组或List结构时,可能需要遍历其中的元素,时间复杂度就是O(n)。
此时,我们可能会使用到Map或Set的容器结构。以Set为例,因为set容器不允许对象重复,如果我们只重写了equal方法,
则两个对象equal相等时,表示两个对象是相等的,但是因为没有重写hashCode方法,那么两个对象的hash值可能不同,如果hash值不同,
是会被放入Set容器的,这就与预期结果矛盾了。
HashMap中,元素的存取是先通过hashCode找到Node数组的下标,然后通过equals方法来比较key是否相同。如果只重写equals方法,equals相同时,
hashCode未必相同。
关于hash的一点说明:
1)相同的对象,hashCode一定相同
2)不同的对象,hashCode可能相同
3)相同的hashCode,对象可能不同
4)不同的hashCode,对象一定不同
3、HashMap底层使用的数据结构,为什么要用这些数据结构
HashMap底层材料 数组 + 链表 + 红黑树 的数据结构,其中链表有 单链表 和 双向链表。双向链表存在于红黑树中。
数组的特点是:内存中的连续地址,查询时间复杂度 O(1) , 插入/删除时间复杂度O(n)【涉及数组下标变动】,比如ArrayList
链表的特点是:内存中不连续的地址,查询时间复杂度O(n),插入/删除时间复杂度O(1),比如LinkedList底层使用链表结构存储【这里是双向链表】
红黑树相较于链表,查询的时间复杂度是O(logn)
数组是HashMap的主容器,根据存入的key,计算出hash值,并与数组长度 - 1 进行 & 运算,能够快速获取元素节点在数组中的位置,完成元素的存取。
链表是为了解决不同的节点计算出的数组下标相同而引入的(包括了hash冲突)。
红黑树是为了解决链表过长时,导致的查询效率下降而引入的。
如果链表中元素数量较少时,链表查询时间复杂度也不会太高,而红黑树的插入和删除操作效率低于链表,所以红黑树是对链表过长操作的优化,
而非完全替代链表。
4、HashMap中的加载因子为什么是 0.75
选择0.75作为加载因子,是一种时间和空间成本的折中选择,出于性能和容量之间平衡的结果。
当加载因子较大的时候,总体空间占用率是提高了,但发生 hash冲突的几率就会大大提升,这会使底层的链表及红黑树变得复杂,导致整体查询效率低下。
当加载因子较小的时候,hash冲突表少了,查询效率高了,但总体空间占用率比较低,且会提升扩容的几率。
负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。
【至于为什么一定是0.75。与统计学中 泊松分布 有关】
5、hash算法为什么要无符号右移16位,计算hash值时,为什么使用 ^ 操作符,而不用 | 及 & 操作符
1)数组下标的计算规则 是 通过key计算出hash值,然后与【数组容量-1】 做 & 运算
2)大量研究表明,HashMap的绝大部分使用场景中,数组容量值都会小于 2^16即65536。
3)当数组的容量很小时,相当于只有低16位的hash值参与运算,如果无符号右移16位后,相当于高16位也参与到hash运算当中,能够更好的均匀散列,
减少hash冲突,提升整体查询性能
4)之所以使用 ^ 操作符,是因为 ^ 操作的结果得到 0 和 1 的概率均为50%,其他的操作符会倾斜向0或1,不利于均匀散列
6、HashMap的容量一定是2的n次幂 大小吗,为什么?
是的
如果我们在创建HashMap对象时,没有提供初始容量,那么在容器第一次放入元素时,会调用扩容方法,初始化一个容量16的Node数组
如果我们在创建HashMap对象时,提供了初始容量,而这个容量又非2的n次幂大小,容器在new对象的时候,会调用一个tableSizeFor(int cap)方法,
该方法会返回一个比给定的容量值大的最近的2的n次幂的数值,并将该值作为扩容阈值。那么在容器第一次放入元素时,会调用扩容方法,
初始化一个容量为扩容阈值的Node数组,并重新计算扩容阈值
// 根据给定的容量,计算出大于等于给定容量的2的最小次幂 值,留待首次放入元素时,初始化容量使用
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
// Integer.numberOfLeadingZeros(cap - 1) 计算出给定容量值的二进制数前面的0的个数
// -1 >>> Integer.numberOfLeadingZeros(cap - 1) 计算出大于等于给定容量的【2的最小次幂值 - 1】
// 返回的是 默认的最大容量 或 【2的最小次幂值】
7、HashMap使用红黑树的过程中,为何要使用双向链表
8、链表向树转换的阈值为什么是 8
之所以选择阈值为8,是出于对时间和空间的一种平衡。TreeNode节点所占空间是Node节点的2倍。
虽然红黑树的查询时间复杂度是O(logN),但在链表长度很短的时候,链表的查询时间复杂度也不高,同时还会节省一半的空间。
在负载因子为0.75的情况下,单个桶内放8个元素的概率为千万分之6,已经非常低了,这时若真的碰撞发生了8次,
说明后序发生hash碰撞的也会比较高。所以,需要将链表转换为红黑树。
9、扩容的原理
10、每次扩容一定是原容量的2倍吗,为什么?
二、红黑树
1、红黑树简介
红黑树是一种二叉搜索树,是计算机科学中用到的一种数据结构,在二叉搜索树的基础上,每个结点增加一个存储位表示结点的颜色,颜色分为红色和黑色。
2、红黑树特性
性质1:每个节点要么是黑色,要么是红色
性质2:根节点是黑色
性质3:每个叶子节点(NIL)是黑色
性质4:每个红色节点的两个子节点一定都是黑色,不能有两个红色节点相连
性质5:任意一节点到每个子节点的路径都包含数量相同的黑节点。俗称:黑高
性质5.1:如果一个节点存在黑子节点,那么该节点肯定有两个子节点。
3、树中常见的概念
1)路径:顺着节点的边从一个节点走到另一个节点,所经过的节点的顺序排列就称为路径
2)根:树顶端的节点称为根。一棵树只有一个根,如果要把一个节点和边的集合称为树,那么从根到其他任何一个节点都必须有且只有一条路径。
3)父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点
4)子节点:一个节点含有的子树的节点称为该节点的子节点
5)兄弟节点:具有相同父节点的节点互称为兄弟节点
6)叶节点:没有子节点的节点称为叶子节点
7)子树:每个节点都可以作为子树的根,它和它所有的子节点、子节点的子节点等都包含在子树中
8)节点的层次:从根开始定义,根为第一层,根的子节点为第二层,以此类推
9)深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0(从上往下看)
10)高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0(从下往上看)
4、为什么要有红黑树
1)二叉搜索树不平衡时(最严重时,退化成链表),可能会严重影响查询效率,时间复杂度从O(logN)退化成O(n)
2)自平衡二叉搜索树解决了二叉搜索树退化为近似链表的缺点,能够把查找时间控制在O(logN),不过却不是最佳的,
因为平衡树要求每个节点的左子树和右子树的高度差至多不等于1,这个要求实在是太严了,导致每次进行插入或删除节点的时候,
几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次称为一颗符合要求的平衡树。
显然,如果在那种插入、删除很频繁的场景中,平衡树需要频繁的进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是出现了红黑树
5、红黑树的实现
5.1 红黑树的平衡操作
红黑树的平衡操作:左旋,右旋和变色
1)变色:节点的颜色红变黑或黑变红
2)左旋:以某个节点作为支点(旋转节点),其右子节点变为旋转节点的父节点,右子节点的左子节点变为旋转节点的右子节点,其他不变
3)右旋:以某个节点作为支点(旋转节点),其左子节点变为旋转节点的父节点,左子节点的右子节点变为旋转节点的左子节点,其他不变
5.2 红黑树为什么一定是红色插入
黑色插入一定会破坏红黑树的平衡性,红色插入可能会破坏,所以,采用红色插入,可尽量减少红黑树的平衡操作,提高插入性能
5.3 红黑树插入节点情景分析
情景1:红黑树为空
1)插入节点作为根节点
2)插入的节点设为黑色
情景2:插入节点的key已存在
1)更新key对应的值即可
情景3:插入节点的父节点为黑节点
1)由于插入的节点是红色的,当插入节点的父节点为黑色时,并不会影响红黑树的平衡,直接插入即可,无需做自平衡
情景4:插入节点的父节点为红色
注意:如果插入节点的父节点为红色,那么该父节点不可能为根节点,所以插入的节点总是存在祖父节点。
4.1:叔叔节点存在并且为红色
因为不可以同时存在两个相连的红节点,那么此时该插入子树的红黑层数的情况是,黑红红。显然最简单的处理方式是把其改为红黑红
处理:
1)将父节点及叔叔节点改为黑节点
2)将祖父节点改成红
3)将祖父节点设置为当前节点,进行后续处理
4.2:叔叔节点不存在或为黑节点,并且插入节点的父节点是祖父节点的左子节点
单纯从插入前来看,叔叔节点非红即空(NIL节点),否则的话就破坏了红黑树性质5,此路径会比其他路径多一个黑色节点。
4.2.1:新插入节点,为其父节点的左子节点(LL红色情况)
1)变颜色,将父节点设置为黑色,将祖父节点设置为红色
2)对祖父节点进行右旋操作
4.2.2:新插入节点,为其父节点的右子节点(LR红色情况)
1)对父节点进行左旋
2)将父节点设置为当前节点,进行下一轮处理
4.3:叔叔节点不存在或为黑节点,并且插入的节点的父节点是祖父节点的右子节点
同4.2反向
4.3.1:新插入节点,为其父节点的右子节点(RR红色情况)
1)变颜色,将父节点设置为黑色,将祖父节点设置为红色
2)对祖父节点进行左旋
4.3.2:新插入节点,为其父节点的左子节点(RL红色情况)
1)对父节点进行右旋
2)将父节点设置为当前节点,进行下一轮处理
5.4 红黑树的结构实现
/**
* 红黑树
*/
public class RBTree<K extends Comparable<K>, V> {
private static final boolean RED = true;
private static final boolean BLACK = false;
/**
* 根节点
*/
private RBNode root;
public RBNode getRoot() {
return root;
}
/**
* 获取当前节点的父节点
* @return
*/
private RBNode parentOf(RBNode node) {
if (node != null) {
return node.parent;
}
return null;
}
/**
* 节点是否为红色
* @return
*/
private boolean isRed(RBNode node){
if (node != null) {
return node.color == RED;
}
return false;
}
/**
* 设置为红色
* @param node
*/
private void setRed(RBNode node){
if (node != null) {
node.color = RED;
}
}
/**
* 节点是否为黑色
* @return
*/
private boolean isBlack(RBNode node){
if (node != null) {
return node.color == BLACK;
}
return false;
}
/**
* 设置为黑色
* @param node
*/
private void setBlack(RBNode node) {
if (node != null) {
node.color = BLACK;
}
}
/**
* 中序打印二叉树
* @return
*/
public void inOrderPrint(){
inOrderPrint(this.root);
}
private void inOrderPrint(RBNode node) {
if (node != null) {
inOrderPrint(node.left);
System.out.println("key:" + node.key + ", value:" + node.value);
inOrderPrint(node.right);
}
}
/**
* 左旋
* 左旋示意图:左旋x节点
* P P
* | |
* x y
* / \ --------> / \
* lx y x ry
* / \ / \
* ly ry lx ly
* 1. 将y左子节点ly的父节点更新为x,将x的右子节点指向y的左子节点ly
* 2. 当x的父节点不为空时,更新y的父节点为x的父节点,并将x的父节点 指向当前x的位置,指向为y
* 3. 将x的父节点更新为y,将y的左子节点更新为x
*/
private void leftRotate(RBNode x){
// 1.获取右子节点
RBNode y = x.right;
// 父节点的右子节点 变更成 右子节点的 左子节点
x.right = y.left;
if (y.left != null) {
y.left.parent = x;
}
if (x.parent != null) {
y.parent = x.parent;
if (x == x.parent.left) {
x.parent.left = y;
} else {
x.parent.right = y;
}
} else { // 说明x为根节点,此时需要更新根节点为y
this.root = y;
this.root.parent = null;
}
x.parent = y;
y.left = x;
}
/**
* 右旋
* 右旋示意图:右旋y节点
* P P
* | |
* y x
* / \ --------> / \
* x ry lx y
* / \ / \
* lx ly ly ry
* 1. 将x右子节点ly父节点更新为y,将y的左子节点指向x的右子节点
* 2. 当y的父节点不为空时,更新x的父节点为y的父节点,并将y的父节点指向当前y的位置,指向为x
* 3. 将y的父节点更新为x,将x的右子节点更新为y
* @param y
*/
private void rightRotate(RBNode y) {
// 获取x节点
RBNode x = y.left;
// 1. 将x右子节点ly父节点更新为y,将y的左子节点指向x的右子节点
y.left = x.right;
if (x.right != null) {
x.right.parent = y;
}
// 2. 当y的父节点不为空时,更新x的父节点为y的父节点,并将y的父节点指向当前y的位置,指向为x
if (y.parent != null) {
x.parent = y.parent;
if (y == y.parent.left) {
y.parent.left = x;
} else {
y.parent.right = x;
}
} else {
this.root = x;
this.root.parent = null;
}
// 3. 将y的父节点更新为x,将x的右子节点更新为y
y.parent = x;
x.right = y;
}
/**
* 对外节点插入数据
* @param key
* @param value
*/
public void insert(K key, V value) {
RBNode node = new RBNode();
node.setKey(key);
node.setValue(value);
node.setColor(RED);
insert(node);
}
/**
* 私有节点插入数据
* @param node
*/
private void insert(RBNode node) {
// 第一步:查找当前node的父节点
RBNode parent = null;
RBNode x = this.root;
while (x != null) {
parent = x;
int cmp = node.key.compareTo(x.key);
if (cmp > 0) {
x = x.right;
} else if (cmp == 0) {
x.setValue(node.getValue());
return;
} else {
x = x.left;
}
}
node.parent = parent;
// 判断node与parent的key 谁大
if (parent != null) {
int cmp = node.key.compareTo(parent.key);
if (cmp > 0) {
parent.right = node;
} else {
parent.left = node;
}
} else {
this.root = node;
}
// 需要调用红黑树修复平衡的方法
insertFixUp(node);
}
/**
* 修复插入带来的红黑树平衡问题
* 红黑树插入节点情景分析
* 情景1:红黑树为空 需要处理
* 1)插入节点作为根节点
* 2)插入的节点设为黑色
* 情景2:插入节点的key已存在 不需要处理
* 1)更新key对应的值即可
* 情景3:插入节点的父节点为黑节点 不需要处理
* 1)由于插入的节点是红色的,当插入节点的父节点为黑色时,并不会影响红黑树的平衡,直接插入即可,无需做自平衡
* 情景4:插入节点的父节点为红色 需要处理
* 注意:如果插入节点的父节点为红色,那么该父节点不可能为根节点,所以插入的节点总是存在祖父节点。
* 4.1:叔叔节点存在并且为红色
* 因为不可以同时存在两个相连的红节点,那么此时该插入子树的红黑层数的情况是,黑红红。显然最简单的处理方式是把其改为红黑红
* 处理:
* 1.将父节点及叔叔节点改为黑节点
* 2.将祖父节点改成红
* 3.将祖父节点设置为当前节点,进行后续处理
* 4.2:叔叔节点不存在或为黑节点,并且插入节点的父节点是祖父节点的左子节点
* 单纯从插入前来看,叔叔节点非红即空(NIL节点),否则的话就破坏了红黑树性质5,此路径会比其他路径多一个黑色节点。
* 4.2.1:新插入节点,为其父节点的左子节点(LL红色情况)
* 1)变颜色,将父节点设置为黑色,将祖父节点设置为红色
* 2)对祖父节点进行右旋操作
* 4.2.2:新插入节点,为其父节点的右子节点(LR红色情况)
* 1)对父节点进行左旋
* 2)将父节点设置为当前节点,得到LL红色情况,进行下一轮处理
* 4.3:叔叔节点不存在或为黑节点,并且插入的节点的父节点是祖父节点的右子节点
* 同4.2反向
* 4.3.1)新插入节点,为其父节点的右子节点(RR红色情况)
* 1)变颜色,将父节点设置为黑色,将祖父节点设置为红色
* 2)对祖父节点进行左旋
* 4.3.2)新插入节点,为其父节点的左子节点(RL红色情况)
* 1)对父节点进行右旋
* 2)将父节点设置为当前节点,得到RR红色情况,接下来处理同4.3.1
* @param node 当前节点
*/
private void insertFixUp(RBNode node){
// 情景1
this.root.setColor(BLACK);
RBNode parent = parentOf(node);
RBNode gParent = parentOf(parent);
// 情景4
if (parent != null && isRed(parent)) {
RBNode uncle = null;
if (parent == gParent.left) { // 父节点为祖父节点的左子节点
uncle = gParent.right;
// 情景4.1 叔叔节点存在并且为红色
if (uncle != null && isRed(uncle)) {
setBlack(parent);
setBlack(uncle);
setRed(gParent);
// 以祖父节点为当前节点,递归处理
insertFixUp(gParent);
return;
}
// 情景4.2 叔叔节点不存在或为黑节点,并且插入节点的父节点是祖父节点的左子节点
if (uncle == null || isBlack(uncle)) {
// 4.2.1 新插入节点,为其父节点的左子节点(LL红色情况)
if (node == parent.left) {
setBlack(parent);
setRed(gParent);
rightRotate(gParent);
return;
}
// 4.2.2 新插入节点,为其父节点的右子节点(LR红色情况)
if (node == parent.right) {
leftRotate(parent);
insertFixUp(parent);
return;
}
}
} else { // 父节点为祖父节点的右子节点
uncle = gParent.left;
// 情景4.1
if (uncle != null && isRed(uncle)) {
setBlack(parent);
setBlack(uncle);
setRed(gParent);
// 以祖父节点为当前节点,递归处理
insertFixUp(gParent);
return;
}
// 情景4.3 叔叔节点不存在或为黑节点,并且插入的节点的父节点是祖父节点的右子节点
if (uncle == null || isBlack(uncle)) {
// 4.3.1 新插入节点,为其父节点的右子节点(RR红色情况)
if (node == parent.right) {
setBlack(parent);
setRed(gParent);
leftRotate(gParent);
return;
}
// 4.3.2 新插入节点,为其父节点的左子节点(RL红色情况)
if (node == parent.left) {
rightRotate(parent);
insertFixUp(parent);
return;
}
}
}
}
}
/**
* 定义静态内部类,红黑树节点
* @param <K>
* @param <V>
*/
static class RBNode<K extends Comparable<K>, V> {
private RBNode parent;
private RBNode left;
private RBNode right;
private boolean color;
private K key;
private V value;
public RBNode(){
}
public RBNode(RBNode parent, RBNode left, RBNode right, boolean color, K key, V value) {
this.parent = parent;
this.left = left;
this.right = right;
this.color = color;
this.key = key;
this.value = value;
}
public RBNode getParent() {
return parent;
}
public void setParent(RBNode parent) {
this.parent = parent;
}
public RBNode getLeft() {
return left;
}
public void setLeft(RBNode left) {
this.left = left;
}
public RBNode getRight() {
return right;
}
public void setRight(RBNode right) {
this.right = right;
}
public boolean isColor() {
return color;
}
public void setColor(boolean color) {
this.color = color;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
}
三、HashMap放入键值对的过程详解
1、 过程的文字描述
1)计算出待放入key的hash值
2)Node数组如果为null或长度为空,则进行扩容
3)根据计算出来的hash与map中维护的【数组长度减一】进行&操作(功能上等同于取模操作),计算出的值作为待放入对象在数组中的下标
4)将key,value及hash值封装成Node节点
5)如果计算出来的数组下标位置没有节点,则将封装的Node节点直接放入数组下标位置
6)如果计算出来的数组下标位置有节点(hash值相同)
6.1) key相同(不论链表还是红黑树)
直接将key对应的新值覆盖旧值
6.2) key不同
如果数组下标节点是TreeNode,则将新的节点放入红黑树中。
如果数组下标节点是普通Node,则将新的节点放入链表中,如果链表的长度超过8位并且数组容量大于64时,则将链表转成红黑树,否则进行扩容处理。
7)如果数组中的元素总数达到阈值(容量的0.75倍),则触发扩容
2、过程的逻辑图示
3、整个过程的源码详解
// 将键值对放入容器
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; // 赋值当前容量。 resize为扩容方法
}
if ((p = tab[i = (n - 1) & hash]) == null) {// 数组中没有i位置的元素,直接添加
// p 是 通过key进行hash,然后再与运算求出数组下标后,下标对应的数组元素
tab[i] = newNode(hash, key, value, null);
} else { // i位置已经有元素了,即p非null
Node<K,V> e;
K k;
// p和hash属性值 就是key的hash运算结果
// 判断已经存在的元素,其hash属性值等于传入的hash属性值
// 其key的属性值和传入的相等,或equals
// 这时候 e = p;【0】
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是普通节点,将树放入链表中,循环链表上的所有节点
for (int binCount = 0; ; ++binCount) {
// 如果是最后一个节点了,
if ((e = p.next) == null) {// 这里先赋值 p.next 给e【1】
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; // 这里又将e再赋值给p【2】
// 经过【1】【2】两步后,循环的位置已经后移1位了。
}
}
// e 之所以会出现不为null的情况,是因为有key相同的情况了。
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();
}
// 该方法在HashMap中是空实现,留给子类,如LinkedHashMap实现
afterNodeInsertion(evict);
return null;
}
// 将键值对放入红黑树中
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {
// 定义一个Class对象变量,这里是用来比较key的大小
Class<?> kc = null;
// 是否查询过左/右子树,只需要查询一次即可
boolean searched = false;
// 定义根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
// 从根节点开始循环
for (TreeNode<K,V> p = root;;) {
// 定义节点添加方向 dir,当前循环节点hash值ph,当前循环节节点的key值pk
int dir, ph; K pk;
// 比较循环节点的hash值 与 传入的hash
if ((ph = p.hash) > h) { // 大于传入的hash值,则放在循环节点的左边
dir = -1;
} else if (ph < h) { // 小于传入的hash值,则放在循环节点的右边
dir = 1;
}else if ((pk = p.key) == k || (k != null && k.equals(pk))) { // 发生hash碰撞时,如果key相等或equals,则直接返回当前循环节点
return p;
// 如果key实现了Comparable接口,并且key是相同的类,直接调用compareTo方法
// 如果compareTo返回0,或者没有实现Comparable接口,或者实现Comparable接口,但key的类不是同一个,则需要比较左/右子树中的节点,
// 如果左/右子树中的节点也没有相同的,则直接比较两个对象的内存地址hashCode值
} else if ((kc == null && (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);
}
// 定义 xp 等于 当前循环节点 p
TreeNode<K,V> xp = p;
// 依据dir的大小,确定 p 的方向,并重新赋值为 p的子节点 值
if ((p = (dir <= 0) ? p.left : p.right) == null) { // 如果为null,则放入。不为null,则做下一次循环。此时的p已经被赋值为p.left或p.right了,即向子节点移动一位
Node<K,V> xpn = xp.next; // 定义当前循环节点的 下一个节点 xpn
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); // 创建新的TreeNode节点
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;
}
// balanceInsertion 用来平衡插入节点后的二叉树
// moveRootToFront 用来将新红黑树的root节点移动到数组tab,对应的下标上
// 以上两个方法扩容源码会详细解释
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
/**
* 这个方法是TreeNode类的一个实例方法,调用该方法的也就是一个TreeNode对象,
* 该对象就是树上的某个节点,以该节点作为根节点,查找其所有子孙节点,
* 看看哪个节点能够匹配上给定的键对象
* h k的hash值
* k 要查找的对象
* kc k的Class对象
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// 把当前对象赋给p,表示当前节点
TreeNode<K,V> p = this;
do {
// 定义当前节点的hash值 ph,方向dir,当前节点的key对象pk
int ph, dir; K pk;
// 定义当前节点的左节点pl,右节点pr,及可能存在的返回值q
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h) { // 当前节点的hash值与传入的hash比较
p = pl; // 当前节点的hash,大于传入的hash,则将当前节点赋值为 当前节点的左节点,进行下一轮比较
} else if (ph < h) {
p = pr; // 当前节点的hash,小于传入的hash,则将当前节点赋值为 当前节点的右节点,进行下一轮比较
} else if ((pk = p.key) == k || (k != null && k.equals(pk))) { // 发生hash碰撞时,如果key相等或equals,则直接将当前节点返回出去
return p;
} else if (pl == null) { // 发生hash碰撞,且key不相等,如果左节点为null,直接将右节点赋值给当前节点
p = pr;
} else if (pr == null) { // 发生hash碰撞,且key不相等,如果右节点为null,直接将左节点赋值给当前节点
p = pl;
// 发生hash碰撞,且key不相等, 左右节点都不为null
// 当前的key实现了Comparable接口 并且 当前节点的key对象与传入的kc对象是属同一个类,且当前节点的key对象与传入的key对象k,compareTo的结果不是0
} 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) { // 如果依然判断不出p的取值,则直接使用右子节点递归查找,查找到了,则直接返回查找的节点对象
return q;
} else {// 如果依然没有查找到节点对象,则将当前节点赋值为 当前节点的左节点,进行下一轮比较
p = pl;
}
} while (p != null);
return null; // 遍历完依然没有查询到,则返回null
}
// 树化操作
final void treeifyBin(Node<K,V>[] tab, int hash) {
// 定义数组长度,节点索引位置及链表的起始节点
int n, index; Node<K,V> e;
/*
* 如果元素数组为空 或者 数组长度小于 树结构化的最小限制
* MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换
* 当一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后结果相同。(并不是因为这些key的hash值相同)
* 因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上。
*/
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; // 定义头节点和尾结点
// 这里的循环主要是将普通节点转成TreeNode节点,并维护双向链表
do {
// 将普通节点 e 转成TreeNode节点 p
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null) { // 尾结点为空,表示是第一个元素
hd = p; // 赋值头结点
} else {
p.prev = tl; // p节点的上一个节点为树链表的尾
tl.next = p; // 树链表尾部的下一个节点指向p
}
tl = p; // 尾结点指向p
} while ((e = e.next) != null); // 判断是否存在下一个元素,继续向链表尾部循环
// 数组下标元素节点不为空,进行树化操作
if ((tab[index] = hd) != null) {
hd.treeify(tab);
}
}
}
四、HashMap的扩容过程
1、过程的文字描述
2、过程的逻辑图示
3、整个过程的源码详解
// 扩容
final Node<K,V>[] resize() {
// 定义一个数组,将老的数组赋值过来
Node<K,V>[] oldTab = table;
// 计算老的容量,如果老的数组为null,直接返回0,否则返回数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 定义老的阈值,默认情况下为0
int oldThr = threshold;
// 定义新的数组容量,新的阈值,默认为0
int newCap, newThr = 0;
// 判断老的容量是否大于0
if (oldCap > 0) {
// 判断老的容量是否大于最大容量,如果等于最大容量,则改变下阈值,因为已经无法再扩容了,所以不在进行扩容动作。
if (oldCap >= MAXIMUM_CAPACITY) {
// 阈值设置为 最大整数值
threshold = Integer.MAX_VALUE;
// 返回老的数组
return oldTab;
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) { // 将newCap赋值为oldCap的两倍,判断newCap是否小于最大容量 并且oldCap要大于等默认初始容量【1】
// 新的阈值newThr,赋值为老的阈值oldThr的两倍
newThr = oldThr << 1; // 模糊的计算出扩容阈值。容量比较低的时候,采用精确计算,容量超过默认的初始化容量16时,采用模糊计算阈值。这样尽可能精确并节省计算时间
// 之所以说这里是模糊计算,是因为按照扩容阈值的计算规则【thr = loadFactor * capacity】,存在 newThr = 2 * odlThr + 1 的情况,比如阈值是0.9。从4(精确阈值3)扩到8(精确阈值7)
}
// 判断老的阈值是否大于0
} else if (oldThr > 0){ // 【使用单个初始容量参数构建的HashMap对象时,第一次放数据会走次分支】【2】
// 设置新的容量等于老的阈值
newCap = oldThr;
} else {【使用无参构造函数创建的HashMap对象时,第一次放数据会走次分支】
// 新的容量等于默认容量
newCap = DEFAULT_INITIAL_CAPACITY;
// 新的阈值等于默认容量乘以加载因子
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) { // 如果新的阈值等于0。只有出现【1】或【2】的情况,才会进入。这里是精确的计算出扩容阈值,精确计算的运算效率比模糊运算效率低
// 定义浮点变量 赋值 为 新容量 乘以 加载因子
float ft = (float)newCap * loadFactor;
// 新阈值 等于 (新容量newCap 及 新容量乘以加载因子值ft 都小于最大容量时,取ft,否则取最大整数值)
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
// 将新的阈值 赋值 给 threshold
threshold = newThr;
// 使用新的容量创建新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将新的数组赋值给table数组变量
table = newTab;
// 如果老的数组不为null
if (oldTab != null) {
// 循环老的数组
for (int j = 0; j < oldCap; ++j) {
// 定义数组元素
Node<K,V> e;
// 判断元素不等于null,等于null,表示数组该下标没有元素,往下走即可
if ((e = oldTab[j]) != null) { // 将老元素赋值给 定义的变量
// 老的数组下标元素置为null
oldTab[j] = null;
// 判断变量 是否存在链表或红黑树结果数据
if (e.next == null){
// 没有链表或红黑树结构时,直接通过 e.hash & (newCap - 1) 算出数组下标,并给新数组newTab下标元素赋值 定义的变量e
newTab[e.hash & (newCap - 1)] = e;
} else if (e instanceof TreeNode) { // 存在红黑树结构时
((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 将红黑树上的节点,重新hash放入新的newTab数组中
} else { // 存在链表结构数据时
Node<K,V> loHead = null, loTail = null; // 低位链表 头和尾 定义
Node<K,V> hiHead = null, hiTail = null; // 高位链表 头和尾 定义
Node<K,V> next; // 定义链表中当前元素的下一个元素
do {
next = e.next; // 下一个元素 等于 当前元素的下一个元素指向
if ((e.hash & oldCap) == 0) { // 元素新数组中的下标不变【原因】
/*
首先,索引的寻找过程是 hash值 & 【数组容量 -1】。
二进制表示方式如下:
00000000 11010000 00001011 00110101 hash 随意举例
&
00000000 00000000 00000000 00001111 【容量 - 1】 以16容量为例
计算结果为 元素索引 index 。其实也就看后四位计算结果,因为前面都是0
扩容后
00000000 11010000 00001011 001【1】0101 hash 随意举例
&
00000000 00000000 00000000 000【1】1111 【容量 - 1】 容量为32
从两个二进制的计算过程来看,如果括号中的两个位运算结果为0,则 计算出元素索引仍是 index。
如果计算结果为1,则新索引就是 【旧容量 + index】(原来索引是后面四位计算的结果,现在索引是后五位计算的结果了,而最高位的1所代表的值就是未扩容前的容量)
上下两个计算过程唯一不同的地方就是扩容后的二进制位中多出的【1】。而多出1的位置,对应的值同旧容量相同。
所以可以通过如下方式来判断扩容后位置是否变化,即 e.hash & oldCap == 0 表示不变,否则变为 当前index + 旧容量
00000000 11010000 00001011 001【1】0101 hash 随意举例
&
00000000 00000000 00000000 000【1】0000 旧容量
*/
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; // loHead其实是一个链表。链表在重新hash时,最多可以拆成两个链表,要么在原来的位置,要么在 【原来的位置+老的容量】 位置
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead; // hiHead也是一个链表
}
}
}
}
}
return newTab;
}
/**
* 红黑树扩容处理方法。
* TreeNode本身既是红黑树也是双向链表
* 1、循环整个链表方式,根据 e.hash & bit 判断节点应处于新数组的高位链表中还是原位置链表中
* 2、重新处理高低位两个链表(如果存在)
* @param map 扩容的对象
* @param tab 扩容后的数组
* @param index 节点在旧数组中的索引位置
* @param bit 旧数组长度
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
// 定义节点b等于当前节点
TreeNode<K,V> b = this;
// 定义两条链表,低位和高位链表。这里同链表处理类似。
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
// 定义低位树节点数量,高位树节点数量
int lc = 0, hc = 0;
// 定义两个变量 e和next,其中e初始化为b
for (TreeNode<K,V> e = b, next; e != null; e = next) {
// next初始化为e的下一个节点
next = (TreeNode<K,V>)e.next;
// 将e的下一个节点置为null
e.next = null; // 取好e的下一节点后,把它赋值为空,方便GC回收
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) {
if (lc <= UNTREEIFY_THRESHOLD) { // 低位树数量表小于6时,转成链表结构
tab[index] = loHead.untreeify(map); // 去树化操作(将元素TreeNode节点都转换成Node节点)
} else {
tab[index] = loHead; //低位链表,迁移到新数组中下标不变,还是等于原数组到下标,把低位链表整个拉到这个下标下,做个赋值
// 高位链表如果为空,说明旧数组下的红黑树中的元素在新数组中仍然全部在同一个位置,且先后顺序没有改变,也就是注释中的已经树化了,没有必要再次树化;
// 而当高位节点不为空,说明原链表元素被拆分了,且低位红黑树节点个数大于6,不满足转链表条件,需要重新树化。
if (hiHead != null){
/**
* 重新树化操作,因为拆分成新的高低位数据结构了,且新的链表长度大于6,之前的树的特性需要更新
*/
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);
}
}
}
}
// 去树化方法比较简单
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) {
// 将q节点 重新转成普通Node节点
Node<K,V> p = map.replacementNode(q, null);
// 如果尾部节点为null,表示处理第一个节点元素
if (tl == null)
hd = p;
else
tl.next = p; // 尾部节点的下一个节点 指向新生成的node节点
tl = p; // 尾部节点 重新指向 新生成的node节点
}
return hd; // 将新的链表头返回
}
// TreeNode转普通Node
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
// 重新树化方法比较复杂。传入的真实节点可能是Node,也可能是TreeNode,TreeNode本身是双向链表
final void treeify(Node<K,V>[] tab) {
// 定义根节点
TreeNode<K,V> root = null;
// 定义当前节点x及下一个节点next
for (TreeNode<K,V> x = this, next; x != null; x = next) {
// 下一个节点next 赋值为当前节点的下一个节点
next = (TreeNode<K,V>)x.next;
// 赋值完成后,将当前节点的左右节点置空
x.left = x.right = null;
// 情景1:树为空
if (root == null) {
// 当前节点的父节点置空(这里就是根节点,根节点没有父节点)
x.parent = null;
// 根节点必须是黑色节点
x.red = false;
// 当前节点 赋值 给根节点
root = x;
} else {
// 定义k 等于 当前节点的 key值
K k = x.key;
int h = x.hash; // 定义h 为当前节点的 hash值
Class<?> kc = null; // 定义Class类型变量
// 定义树的根节点p
for (TreeNode<K,V> p = root;;) { // 从根节点开始遍历,此遍历没有设置边界,只能从内部跳出
int dir, ph; // dir标识方向(-1:左 1:右) ph表示当前树节点的hash
K pk = p.key;
if ((ph = p.hash) > h) {// 当前节点hash值h 与树节点 hash值 ph比较
dir = -1; // 标识当前链表节点会放到当前树节点的左侧
} else if (ph < h) {
dir = 1; // 右侧
/*
* 如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较
* 如果当前链表节点的key实现了comparable接口,并且当前树节点和链表节点是相同Class的实例,那么通过comparable的方式再比较两者。
* 如果还是相等,最后再通过tieBreakOrder比较一次
*/
} else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) {
dir = tieBreakOrder(k, pk);
}
// 保存当前树节点
TreeNode<K,V> xp = p;
/*
* 如果dir 小于等于0 : 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左孩子,也可能是左孩子的右孩子 或者 更深层次的节点。
* 如果dir 大于0 : 当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右孩子,也可能是右孩子的左孩子 或者 更深层次的节点。
* 如果当前树节点不是叶子节点,那么最终会以当前树节点的左孩子或者右孩子 为 起始节点 再从GOTO1 处开始 重新寻找自己(当前链表节点)的位置
* 如果当前树节点就是叶子节点,那么根据dir的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。
* 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了。
*/
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;
}
}
}
}
// 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪一个节点是不确定的
// 因为我们要基于树来做查找,所以就应该把 tab[N] 得到的对象一定根是节点对象,而目前只是链表的第一个节点对象,所以要做相应的处理。
//把红黑树的根节点设为 其所在的数组槽 的第一个元素
//首先明确:TreeNode既是一个红黑树结构,也是一个双链表结构
//这个方法里做的事情,就是保证树的根节点一定也要成为链表的首节点
moveRootToFront(tab, root);
}
/**
* 打破平局顺序方法,让两个通过各种比较相同的key,分出个先后顺序
*/
static int tieBreakOrder(Object a, Object b) {
int d;
// 如果都不为null,比较两个对象的Class对象名称ASCII码,相同的类型时,在比较对象内存中的地址hashCode值
if (a == null || b == null || (d = a.getClass().getName().compareTo(b.getClass().getName())) == 0) {
/**
* System.identityHashCode方法是java根据对象在内存中的地址算出来的一个数值,不同的地址算出来的结果是不一样的。
* 与对象是否重写hashCode方法无关。其中null的hashCode值为0
*/
d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1);
}
return d;
}
/**
红黑树插入节点情景分析
情景1:红黑树为空
1)插入节点作为根节点
2)插入的节点设为黑色
情景2:插入节点的key已存在
1)更新key对应的值即可
情景3:插入节点的父节点为黑节点
1)由于插入的节点是红色的,当插入节点的父节点为黑色时,并不会影响红黑树的平衡,直接插入即可,无需做自平衡
情景4:插入节点的父节点为红色
注意:如果插入节点的父节点为红色,那么该父节点不可能为根节点,所以插入的节点总是存在祖父节点。
4.1:叔叔节点存在并且为红色
因为不可以同时存在两个相连的红节点,那么此时该插入子树的红黑层数的情况是,黑红红。显然最简单的处理方式是把其改为红黑红
处理:
1.将父节点及叔叔节点改为黑节点
2.将祖父节点改成红
3.将祖父节点设置为当前节点,进行后续处理
4.2:叔叔节点不存在或为黑节点,并且插入节点的父节点是祖父节点的左子节点
单纯从插入前来看,叔叔节点非红即空(NIL节点),否则的话就破坏了红黑树性质5,此路径会比其他路径多一个黑色节点。
4.2.1:新插入节点,为其父节点的左子节点(LL红色情况)
1)变颜色,将父节点设置为黑色,将祖父节点设置为红色
2)对祖父节点进行右旋操作
4.2.2:新插入节点,为其父节点的右子节点(LR红色情况)
1)对父节点进行左旋
2)将父节点设置为当前节点,进行下一轮处理
4.3:叔叔节点不存在或为黑节点,并且插入的节点的父节点是祖父节点的右子节点
同4.2反向
4.3.1)新插入节点,为其父节点的右子节点(RR红色情况)
1)变颜色,将父节点设置为黑色,将祖父节点设置为红色
2)对祖父节点进行左旋
4.3.2)新插入节点,为其父节点的左子节点(RL红色情况)
1)对父节点进行右旋
2)将父节点设置为当前节点,进行下一轮处理
*/
// 节点插入
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
//结点的颜色默认是红色的
x.red = true;
//xp父结点 xpp祖父结点 xppl祖父结点的左结点 xppr祖父结点的右结点
//循环是因为要循环判断在旋转或者变色之后的结构还是否满足红黑树规则,一直到根结点
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
/**
情景1:红黑树为空
1)插入节点作为根节点
2)插入的节点设为黑色
*/
if ((xp = x.parent) == null) {
x.red = false;
return x;
//如果当前结点的上一个结点是黑色的,就停止循环
//因为如果父结点是黑色的,则插入红色结点不会对黑节点的平衡产生影响
/**
情景3:插入节点的父节点为黑节点
1)由于插入的节点是红色的,当插入节点的父节点为黑色时,并不会影响红黑树的平衡,直接插入即可,无需做自平衡
*/
} else if (!xp.red || (xpp = xp.parent) == null){
return root;
}
if (xp == (xppl = xpp.left)) { // 父节点为祖父节点的左子树【叔叔节点在右侧】 为什么判断叔叔节点而不直接判断父节点???
/**
4.1:叔叔节点存在并且为红色
因为不可以同时存在两个相连的红节点,那么此时该插入子树的红黑层数的情况是,黑红红。显然最简单的处理方式是把其改为红黑红
处理:
1.将父节点及叔叔节点改为黑节点
2.将祖父节点改成红
3.将祖父节点设置为当前节点,进行后续处理
*/
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false; // 叔叔节点设置为黑
xp.red = false; // 父节点设置为黑
xpp.red = true; // 祖父节点设置为红
x = xpp; // 当前节点设置为祖父节点,进行下一轮循环
} else { // 叔叔节点不存在或者为黑色
// 之所以先处理LR红的情况,是因为经过LR红情况的处理,会变成LL红情况
/**
4.2.2:新插入节点,为其父节点的右子节点(LR红色情况)
1)对父节点进行左旋
2)将父节点设置为当前节点,进行下一轮处理
*/
if (x == xp.right) {
root = rotateLeft(root, x = xp); // 将当前节点 指向 父节点
xpp = (xp = x.parent) == null ? null : xp.parent; // 将原父节点 指向原祖父节点。将原祖父节点 指向原祖父节点的父节点
}
/**
4.2.1:新插入节点,为其父节点的左子节点(LL红色情况)
1)变颜色,将父节点设置为黑色,将祖父节点设置为红色
2)对祖父节点进行右旋操作
*/
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);
}
}
}
}
}
}
/**
* 左旋
* @param root 当前根节点
* @param p 指定的旋转节点
* @return 返回根节点(平衡涉及左旋右旋会将根节点改变,所以需要返回最新的根节点)
* 左旋示意图:左旋p节点
pp pp
| |
p r
/ \ ----> / \
l r p rr
/ \ / \
rl rr l rl
左旋做了几件事?
* 1、将rl设置为p的右子节点,将rl的父节点设置为p
* 2、将r的父节点设置pp,将pp的左子节点或者右子节点设置为r
* 3、将r的左子节点设置为p,将p的父节点设置为r
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) { // 以当前节点的父节点左旋
// r:旋转节点的右子节点; pp:旋转节点的父节点, rl:旋转节点的右子节点的左子节点
TreeNode<K,V> r, pp, rl;
// 旋转节点非空并且旋转节点的右子节点非空
if (p != null && (r = p.right) != null) {
// 将p节点的右子节点设置为右子节点的左子节点
if ((rl = p.right = r.left) != null) {
// 将rl的父节点设置为p
rl.parent = p;
}
if ((pp = r.parent = p.parent) == null) {//将r的爸爸节点设置为p的爸爸节点,如果是空的话
(root = r).red = false;// 染成黑色,并赋值给root
} else if (pp.left == p) { //判断父节点是爷爷节点的左子节点还是右子节点
pp.left = r; //如果是左子节点,那么就把爷爷节点的左子节点设置为r
} else {
pp.right = r; //如果是右子节点,就把爷爷节点的右子节点设置为r
}
r.left = p; //最后将r的左子节点设置为p
p.parent = r; //将p的爸爸节点设置为r
}
return root;
}
/**
* 右旋
* @param root 当前根节点
* @param p 指定的旋转节点
* @return 返回根节点(平衡涉及左旋右旋会将根节点改变,所以需要返回最新的根节点)
* 右旋示意图:右旋p节点
pp pp
| |
p l
/ \ ----> / \
l r ll p
/ \ / \
ll lr lr r
* 右旋都做了几件事?
* 1.将lr设置为p节点的左子节点,将lr的父节点设置为p
* 2.将l的父节点设置为pp,将pp的左子节点或者右子节点设置为l
* 3.将l的右子节点设置为p,将p的父节点设置为l
*/
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;
}
// 进行双向链表的调整,经过左旋,或者右旋操作后,双向链表的头节点可能发生了变化。需要把根节点放到链表头节点。【树结构是正常的】
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {// root节点不是数组下标元素值
Node<K,V> rn;
tab[index] = root; //设置数组下标元素值为root
TreeNode<K,V> rp = root.prev; // 定义 root 在链表中的前一个元素
if ((rn = root.next) != null) { // root 在链表中的后一个元素。不为空的情况下
((TreeNode<K,V>)rn).prev = rp; // 让后一个元素的前一个元素指向root的前一个元素
}
if (rp != null) { // 不为空的情况下
rp.next = rn; // root的前一个元素 的 后一个元素 指向 root的后一个元素【相当于 a - b - c 链表中,去掉b,让 a - c 互相指向】
}
if (first != null) {
first.prev = root;// 原始的在数组中的元素的前一个元素指向root
}
root.next = first; // root的下一个元素指向原始的在数组中的元素
root.prev = null; // root的前一个元素置空
}
assert checkInvariants(root);
}
}
/**
验证红黑树的准确性
递归检查所有错误情况
前序、后序、父节点、左右节点双向链接检查
连续红节点检查
左右子节点递归检查
*/
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
// 当前结点的父节点,左节点,右结点
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
// 当前结点的前驱结点和后继结点
tb = t.prev, tn = (TreeNode<K,V>)t.next;
//判断当前结点和前驱有没有连起来
if (tb != null && tb.next != t) {
return false;
}
// 判断当前结点和下一个有没有连起来
if (tn != null && tn.prev != t) {
return false;
}
// 判断当前结点和父结点有没有连起来
if (tp != null && t != tp.left && t != tp.right) {
return false;
}
// 判断当前结点和子节点有没有连起来,如果在左边,hash比当前结点的要小
if (tl != null && (tl.parent != t || tl.hash > t.hash)) {
return false;
}
// 判断当前结点和子节点有没有连起来,如果在右边,hash比当前节点的要大
if (tr != null && (tr.parent != t || tr.hash < t.hash)) {
return false;
}
// 红黑树不能两个红色结点相连
if (t.red && tl != null && tl.red && tr != null && tr.red) {
return false;
}
// 递归检测左子树
if (tl != null && !checkInvariants(tl)) {
return false;
}
// 递归检测右子树
if (tr != null && !checkInvariants(tr)) {
return false;
}
return true;
}