JAVA红黑树的增删查模拟
一、红黑树的概念
- 一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
- 红黑树的特性
- 每个节点或者是黑色,或者是红色
- 根节点是黑色
- 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
- 如果一个节点是红色的,则它的子节点和父节点必须是黑色的
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
- 最简单的定义就是二叉查询树,但是有颜色区分,树根一定为黑色,从某个节点到下面的所有分支到每个叶子节点途径过的黑色节点一样多,null节点默认为黑色,红色节点的子节点只能是黑色节点
使用网站红黑树生成网站
- 例如上图中,分别展示NULL叶子节点和无NULL叶子节点
- 但是不管是否显示,从根节点到叶子节点的黑节点个数是一致的
- 0002-0001两个黑节点,0002-0004-0003两个黑节点,其他也同理
二、JAVA红黑树应用
- 应用在HashMap中,当链表长度超过限定长度后,链表结构会自动转化为红黑树,由于需要满足链表和红黑树之间的互相转换,所以HashMap中的红黑树要比一般的红黑树操作更复杂一点
- 应用在TreeMap中,TreeMap的底层就是红黑树,比较则是由存储数据的Comparable或外界传入的Comparator完成的
三、模拟TreeMap完成红黑树的增删查
3.1 红黑树基本结构
- 红黑树节点存在如下变量
- parent 父节点
- right 右节点(就是子节点中的右边节点)
- left 左节点(就是子节点中的左边节点)
- value 存储数据信息(TreeMap中是以[key, value]形式存储数据,主要以key作为比较对象,在此处默认只有key,且数据类型为int)
- black 颜色布尔(默认为黑色节点)
public class Node {
public Node parent;
public Node right;
public Node left;
public int value;
public boolean black = true;
public Node(Node parent, Node right, Node left, int value, boolean black) {
this.parent = parent;
this.right = right;
this.left = left;
this.value = value;
this.black = black;
}
}
3.2 红黑树操作类
public class RedBlackTree {
// 根节点
public Node root;
}
3.3 红黑树增加节点
- 首先是增加节点的插入位置,这个和普通二叉树一样,都是比较值大小,然后确定插入位置,懂得都懂;
- 插入节点时,默认插入的是红色节点,根据情况调整树结构及节点颜色;
- 2.1 如果是首次插入,则插入节点默认是红黑树根节点,颜色就是黑色;
- 2.2 如果不是,则判断插入节点的父节点的节点颜色;
- 2.3 如果父节点是黑节点,对于红黑树的特性来说,增加一个红节点不会改变黑节点的个数,所以可以直接增加对应节点;
- 例如增加一个红节点7,可以直接插入
- 2.4 如果父节点是红节点,再增加一个红节点,就会违反红黑树的红节点的子节点必须是黑节点特性,所以这种情况必须特定处理;
-
如何解决问题,必须要设置子节点或者父节点为黑节点,但是只要设置为黑节点,该节点所在树的左子树/右子树失衡(多了一个黑节点),所以解决问题的措施,就是通过调整颜色和树结构旋转,使得黑节点个数平衡
-
- 2.5 当父节点是祖父节点的左节点时,祖父节点肯定是黑节点,此时需要判断叔父节点的颜色
-
2.5.1 当叔父节点是红节点时,只需要设置父节点和叔父节点为黑节点,祖父节点设置为红节点,对于插入节点到祖父节点的这一部分红黑树来说,是满足特性的,只需要从祖父节点重新开始判定,就可以逐步平衡整棵树的红黑节点;
- 例如增加一个红节点7
- 需要设置8,15为黑色,10为红色,7-10这个红黑树才能平衡,但是10变成红色后,就需要从10节点重新调平;
-
2.5.2 当叔父节点是黑节点时,由于叔父节点无法通过变色手段增加黑节点,就必须要使用旋转来使得红黑树平衡
-
例如增加一个左节点红节点7,下图这种情况本人感觉很难出现,一般而言不会存在下图,除非8.5和15都是NULL节点
-
-
-
如果单纯只设置8为黑色,则左子树就会多一个黑节点,如果再次设置10为红色,则右子树会少一个黑节点,所以还需要对10所在节点,进行右旋转(右旋即是将当前节点转为下级左节点的下级右节点),使得对应红黑树平衡
-
-
当树平衡后,处理结束;
-
-
如果增加一个右节点红节点9
-
- 此时只需要将此种情况,调整为插入节点为左节点,就可以直接使用增加一个左节点红节点7类似逻辑处理
- 只需要对8节点进行左旋转(左旋是将当前节点转为下级右节点的下级左节点),即可调整为上述情况
- 然后将插入节点设置为8节点,采用上述逻辑处理即可;
-
- 2.6 当父节点是祖父节点的右节点时,祖父节点肯定是黑节点,此时需要判断叔父节点的颜色
- 2.6.1 当叔父节点是红节点时,只需要设置父节点和叔父节点为黑节点,祖父节点设置为红节点,对于插入节点到祖父节点的这一部分红黑树来说,是满足特性的,只需要从祖父节点重新开始判定,就可以逐步平衡整棵树的红黑节点;
- 2.6.2 当叔父节点是黑节点时,由于叔父节点无法通过变色手段增加黑节点,就必须要使用旋转来使得红黑树平衡
- 增加一个右节点红节点,只需要设置父节点为黑节点,祖父节点为红节点,并对父节点进行左旋,处理结束
- 增加一个左节点红节点,只需要设置父节点为黑节点,祖父节点为红节点,并对父节点进行右旋,然后对原来的父节点进行重新处理即可。
3.4 红黑树增加节点代码
public void put(int value) {
if (root == null) {
// 插入的第一个节点
root = new Node(null, null, null, value, true);
} else {
// 查找插入节点的父节点
Node current = root;
Node parrent = root;
while (current != null) {
if (value > current.value) {
parrent = current;
current = current.right;
} else if (value < current.value) {
parrent = current;
current = current.left;
} else {
return;
}
}
Node x;
// 设置插入节点和父节点的关系
if (value > parrent.value) {
parrent.right = new Node(parrent, null, null, value, false);
x = parrent.right;
} else {
parrent.left = new Node(parrent, null, null, value, false);
x = parrent.left;
}
// 从插入节点开始对红黑树进行调平
fixNodeActural(x);
}
}
private void fixNodeActural(Node x) {
// 最初的判定当前节点不为null,不是根节点,且父节点的颜色是红色
while (x != null && x != root && !isBlack(x.parent)) {
// 如果父节点是祖父节点的左节点
if (x.parent == x.parent.parent.left) {
// 判断叔父节点是不是红色,是的话同时调整父节点和叔父节点还有祖父节点的颜色
if (!isBlack(x.parent.parent.right)) {
x.parent.black = true;
x.parent.parent.black = false;
x.parent.parent.right.black = true;
// 以祖父节点为重新调整节点
x = x.parent.parent;
} else {
// 如果是黑色,则需要判断当前节点是不是父节点的右节点
if (x == x.parent.right) {
x = x.parent;
// 左旋转
rolateLeft(x);
}
// 设置父节点为黑色,对祖父设置为红色,且右旋转
x.parent.black = true;
x.parent.parent.black = false;
rolateRight(x.parent.parent);
}
} else {
// 如果父节点是祖父节点的右节点
// 判断叔父节点是不是红色,是的话同时调整父节点和叔父节点还有祖父节点的颜色
if (!isBlack(x.parent.parent.left)) {
x.parent.black = true;
x.parent.parent.black = false;
x.parent.parent.left.black = true;
// 以祖父节点为重新调整节点
x = x.parent.parent;
} else {
// 如果是黑色,则需要判断当前节点是不是父节点的左节点
if (x == x.parent.left) {
x = x.parent;
// 右旋转
rolateRight(x);
}
// 设置父节点为黑色,对祖父设置为红色,且左旋转
x.parent.black = true;
x.parent.parent.black = false;
rolateLeft(x.parent.parent);
}
}
}
// 设置根节点颜色为黑色,有可能在调整的过程中根节点被切换了
root.black = true;
}
// 左旋
private void rolateLeft(Node x) {
// 一定存在
Node x3 = x.right;
// 可能为null
Node x1 = x.parent;
// 可能为Null
Node x4 = x3.left;
x3.parent = x1;
if (x1 == null) {
root = x3;
} else if (x1.left == x) {
x1.left = x3;
} else {
x1.right = x3;
}
x.parent = x3;
x3.left = x;
x.right = x4;
if (x4 != null) {
x4.parent = x;
}
}
// 右旋
private void rolateRight(Node x) {
// 一定存在
Node x3 = x.left;
// 可能为null
Node x1 = x.parent;
// 可能为null
Node x4 = x3.right;
x3.parent = x1;
if (x1 == null) {
root = x3;
} else if (x1.right == x) {
x1.right = x3;
} else {
x1.left = x3;
}
x.parent = x3;
x3.right = x;
x.left = x4;
if (x4 != null) {
x4.parent = x;
}
}
protected boolean isBlack(Node x) {
return x == null || x.black;
}
3.5 红黑树查找节点
- 最普通的二叉树查找节点方法即可
private Node findNodeByValue(int value) {
if (root == null) {
return null;
}
Node currentNode = root;
while (currentNode != null) {
if (value > currentNode.value) {
currentNode = currentNode.right;
} else if (value < currentNode.value) {
currentNode = currentNode.left;
} else {
return currentNode;
}
}
return null;
}
3.6 红黑树删除节点
-
首先找到删除节点
-
判断删除节点的左/右节点是否存在
-
如果没有左/右节点,直接删除该节点即可
-
如果只存在一个左/右节点,则使用左/右节点替代该节点,如果被删除节点是黑色,则从替代节点开始,重新调整颜色及结构
-
如果存在两个节点,则需要使用删除节点的后继节点(或者前驱节点),本文使用后继节点
- 按照左-根-右的顺序遍历
- 后继节点是中序遍历该节点的下一个节点,
- 前驱节点是中序遍历该节点的上一个节点;
- 如果该删除节点存在右子树,则说明该右子树的最左子节点就是该删除节点的后继节点
- 如果该删除节点只有左子树,则需要循环判断,当前节点(删除节点)和父节点是否是左子树的关系,如果不是,则需要将父节点作为当前节点,继续向上找,直到找到左子树关系的父节点即为删除节点的后继节点
- 在这里就很简单,后继节点走上面的右子树就行
- 找到该后继节点后,用后继节点的内容替换删除节点的内容,然后删除后继节点
-
找到删除节点后,使用删除节点的左(优先使用,不存在则使用右节点)替代需要删除的节点
-
如果删除节点是黑色,使用的替代节点如果是红色,则直接变更颜色即可
-
-
例如删除节点15,由于15是黑节点,删除后,右子树少一个黑节点,只需要将20红节点颜色变更为黑色即可
-
-
-
如果替代节点是黑色,由于删除节点是黑色,所以相当于当前路径少了一个黑色节点,所在的左/右树整体少一个黑色节点,不符合红黑树定义
-
如果删除节点是左节点
- 9.1 如果兄弟节点是黑节点
- 9.1.1 如果兄弟节点的子节点都是黑色,则需要将兄弟节点变更为红色,从父结点到子节点的两条路径黑色节点个数相同
-
-
例如删除节点5,使用替代节点1,左子树整体会少一个黑节点,需要将节点15变更为红色,才能使左右树黑节点平衡
-
-
如果父节点是红色,则修改为黑色节点,修改结束,此时相当于所在树整体补上了一个黑色节点;
-
如果父节点是黑色,则要从父节点开始重新判断,此时树还是少一个黑色节点
-
- 9.1.2 如果兄弟节点至少有一个子节点是红节点,则进行以下判断
- 9.1.3 如果兄弟节点的右节点是红节点,则将父节点的颜色赋值给兄弟节点,兄弟节点的右节点设置为黑色,对父节点进行左旋操作,这样当前树高度相等,且补充了删除节点的黑色节点个数
- 例如删除节点5,使用替代节点1,左子树整体会少一个黑节点,需要将节点20变更为黑色,并且对树进行旋转,才能使左右树黑节点平衡
- 9.1.4 如果兄弟节点的右节点是黑节点,则左节点肯定是红节点,将兄弟节点设置为红色,兄弟节点的左节点设置为黑色,再对兄弟节点进行右旋转,此时树结构发生变化,变成9.1.3了,进行9.1.3逻辑即可
- 例如删除节点5,使用替代节点1,左子树整体会少一个黑节点,需要将节点13变更为黑色,15变更成红色,并且对树进行旋转,达到9.1.3的水平
- 按照9.1.3的操作,则可让整棵树平衡
- 9.1.1 如果兄弟节点的子节点都是黑色,则需要将兄弟节点变更为红色,从父结点到子节点的两条路径黑色节点个数相同
- 9.2 如果兄弟节点是红节点,则兄弟节点的父节点一定是黑色
- 9.2.1 兄弟节点设置为黑节点,父节点设置为红节点,对父节点进行左旋操作, 则可变更为9.1的逻辑信息,重新以替代节点进行逻辑判断
- 例如删除节点5,使用替代节点1,左子树整体会少一个黑节点,需要将节点15变更为黑色,10变更成红色,并且对树进行旋转,达到9.1的水平
- 按照9.1的操作,最终结果
- 9.2.1 兄弟节点设置为黑节点,父节点设置为红节点,对父节点进行左旋操作, 则可变更为9.1的逻辑信息,重新以替代节点进行逻辑判断
- 9.1 如果兄弟节点是黑节点
-
如果删除节点是右节点
- 10.1 如果兄弟节点是黑节点
- 10.1.1 如果兄弟节点子节点都是黑节点,只需要设置兄弟节点为黑节点
- 如果父节点是红节点,则设置父节点为黑节点,树平衡结束
- 如果父节点是黑节点,则需要从父节点开始重新平衡树
- 10.1.2 如果存在一个子节点是红节点
- 10.1.3如果左节点是红节点,则设置左节点为黑节点,父节点颜色设置给兄弟节点,父节点设置为黑节点,并且父节点右旋,平衡结束
- 10.1.4如果左节点是黑节点,则右节点必定是红节点,设置兄弟节点为红节点,右节点设置为黑节点,并对兄弟节点进行左旋,则变成10.1.3情况,继续处理
- 10.1.1 如果兄弟节点子节点都是黑节点,只需要设置兄弟节点为黑节点
- 10.2 如果兄弟节点是红节点,则兄弟节点的父节点一定是黑节点
- 10.2.1 设置兄弟节点为黑节点,父节点设置为红节点,并对父节点进行右旋,则可变更成10.1的逻辑信息
- 10.1 如果兄弟节点是黑节点
3.7 红黑树删除节点代码
public void delete(int value) {
Node node = findNodeByValue(value);
if (node == null) {
throw new RuntimeException("无法找到删除节点");
}
// 判断存在几个子节点
Node p = node;
if (node.left != null && node.right != null) {
// 两个子节点,则找后继节点
Node sussor = node.right;
while (sussor.left != null) {
sussor = sussor.left;
}
// 后继节点的内容覆盖掉
p.value = sussor.value;
p = sussor;
}
// 获取替代节点
Node replaceNode = p.left != null ? p.left : p.right;
if (replaceNode != null) {
replaceNode.parent = p.parent;
// 开始替换
if (p.parent == null) {
root = replaceNode;
} else if (p == p.parent.left) {
p.parent.left = replaceNode;
} else {
p.parent.right = replaceNode;
}
p.left = p.parent = p.right = null;
if (p.black) {
fixDeleteActural(replaceNode);
}
} else if (p.parent == null) {
// 也不存在父级节点了
root = null;
} else {
// 没有左右节点,删除本身
if (p.black) {
fixDeleteActural(p);
}
if (p == p.parent.left) {
p.parent.left = null;
} else {
p.parent.right = null;
}
p.parent = null;
}
}
private void fixDeleteActural(Node node) {
while (node != root && isBlack(node)) {
if (node == node.parent.left) {
// 如果是左节点,判断兄弟节点的颜色
Node nr = node.parent.right;
// 如果兄弟节点是红色
if (!isBlack(nr)) {
nr.black = true;
node.parent.black = false;
rolateLeft(node.parent);
nr = node.parent.right;
}
// 如果兄弟节点的所有子节点都是黑色,则只需要将兄弟节点变成红色,继续处理父节点即可
if (isBlack(nr.left) && isBlack(nr.right)) {
nr.black = false;
node = node.parent;
} else {
// 兄弟节点的右节点为黑色,则变更颜色及旋转处理
if (isBlack(nr.right)) {
nr.left.black = true;
nr.black = false;
rolateRight(nr);
nr = node.parent.right;
}
nr.right.black = true;
nr.black = node.parent.black;
node.parent.black = true;
rolateLeft(node.parent);
node = root;
}
} else {
// 如果是右节点,判断兄弟节点的颜色
Node nl = node.parent.left;
// 如果兄弟节点是红色
if (!isBlack(nl)) {
nl.black = true;
node.parent.black = false;
rolateLeft(node.parent);
nl = node.parent.left;
}
// 如果兄弟节点的所有子节点都是黑色,则只需要将兄弟节点变成红色,继续处理父节点即可
if (isBlack(nl.left) && isBlack(nl.right)) {
nl.black = false;
node = node.parent;
} else {
// 兄弟节点的左节点为黑色,则变更颜色及旋转处理
if (isBlack(nl.left)) {
nl.right.black = true;
nl.black = false;
rolateLeft(nl);
nl = node.parent.left;
}
nl.left.black = true;
nl.black = node.parent.black;
node.parent.black = true;
rolateRight(node.parent);
node = root;
}
}
}
if (node != null) {
node.black = true;
}
}