红黑树(red black tree)
特点
红黑树是一种自平衡的二叉查找树.
-
每个节点要么是红色要么是黑色。
-
根节点是黑色。
-
每个叶子节点都是黑色的空节点(NIL)。这里的黑色空节点是虚拟出来的,不是实际的节点。即在每个叶子节点上添加一个黑色空节点。
-
任何相邻的节点不能同时为红色,如果一个节点是红色,那么其父节点、子节点必定是黑色。
-
每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点。
上面说的第三条就是在每个叶子节点虚构一个NIL空节点。
旋转
在红黑树或其他自平衡二叉搜索树中,左右旋转是一种用于保持树平衡的操作。
左旋
1、将节点的右子树上升成为父节点。
2、将新的父节点的原左子节点变为原节点的右子节点。
3、原节点下沉为新父节点的左子树。
示例:
左旋前
50
\
70
/ \
60 80
对节点50进行左旋后
70
/ \
50 80
\
60
在左旋操作后,节点70成为新的子树根,50变为70的左子节点,70的左子树(60)变为为50的右子树。
左旋用于处理右重或不平衡情况,帮助恢复树的平衡。
java代码
void rotateLeft(Node x){
Node y = x.right;
if(y == null) return;
//将当前节点右子节点的左子节点变为当前节点的右子节点
x.right = y.left;
if(y.left != null){
y.left.parent = x;
}
//将当前节点的右子节点与其父节点建立父子关系
y.parent = x.parent;
if(x.parent == null){
root = y;
}else if(x == x.parent.left){
x.parent.left = y;
}else{
x.parent.right = y;
}
//当前节点变为其右子节点的左子节点
x.parent = y;
y.left = x;
}
右旋
1、将当前节点的左子节点提升为新的父节点。
2、将原节点下沉到新父节点的右子树位置。
3、 将新的父节点的右子节点变为当前节点的左子节点。
右旋示例:
旋转前:
50
/
30
/ \
20 40
以节点50进行右旋后:
30
/ \
20 50
/
40
- 30 成为新的根节点:因为
30
是50
的左子节点,经过右旋后,30
成为新的根节点。 - 50 成为 30 的右子节点:原根节点
50
变成了30
的右子节点。 - 40 成为 50 的左子节点:原本是
30
的右子节点,现在成了50
的左子节点。
右旋代码
void rotateRight(Node x){
Node y = x.left;
if(y == null) return;
//将Y的右子节点变为x的左子节点
x.left = y.right;
if(y.right != null){
y.right.parent = x;
}
//将Y节点与X父节点建立父子关系
y.parent = x.parent;
if(x.parent == null){
root = y;
}else if(x == x.parent.left){
x.parent.left = y;
}else{
x.parent.right = y;
}
//将x变为Y节点的右子节点
x.parent = y;
y.right = x;
}
插入
红黑树的插入首先根据搜索树的特点找到插入元素应该插入的位置,然后默认设置插入的节点是红色。
如果插入节点是根节点则只需变换节点颜色即可。
如果插入接的父节点是黑色,则直接插入节点即可。
当插入节点的父节点是红色的时候,这样插入后就会违背红黑树的特性,这个时候就需要进行一些颜色的变换或旋转操作,使其符合红黑树的特性。分三种情况进行处理:
case1、父节点的兄弟节点(叔叔节点)是红色
将父节点及叔叔节点变为黑色
将祖父节点变为红色
将当前节点指向祖父节点继续进行判断
case2、叔叔节点是黑色且当前节点是右子节点
将当前节点指向其父节点,将父结点进行左旋。
然后跳转至case3
case3、叔叔节点是黑色且当前节点是左子节点
把父结点变为黑色
把祖父节点变为红色
以祖父节点为支点进行右旋。调整结束。
插入代码
首先定义一个节点类
static class Node{
private int val;
private Node left;
private Node right;
private Node parent;
private int color;
Node(int val) {
this.val = val;
this.color = RED;
}
}
该节点类描述了节点相关的树位置信息及红黑树特有的属性颜色。
首先插入一个节点,按搜索树的特点找到插入元素应该插入的位置,不考虑颜色关系。
void doInsert(Node root, Node node){
if(root.val > node.val){
if(root.left == null){
root.left = node;
node.parent = root;
}else{
doInsert(root.left,node);
}
}
if(root.val < node.val){
if(root.right == null){
root.right = node;
node.parent = root;
}else{
doInsert(root.right,node);
}
}
}
然后在根据上面的插入规则进行颜色的变化和旋转进行树的平衡操作
void balanceTree(Node node){
if(node == root){
node.color = BLACK;return;
}
if(node.parent == root){
return;
}
//父节点是红色
while (node.parent != null && node.parent.color == RED){
//找叔叔节点
Node uncle = null;
//父节点是左子节点
if(node.parent == node.parent.parent.left){
//叔叔节点是右子节点
uncle = node.parent.parent.right;
}else{
//叔叔节点是左子节点
uncle = node.parent.parent.left;
}
//叔叔节点是红色
if(uncle != null && uncle.color == RED){
//变色操作 父节点叔叔节点变为黑色,祖父节点变为红色
node.parent.color = BLACK;
uncle.color = BLACK;
node.parent.parent.color = RED;
//将指针指向祖父节点继续
node = node.parent.parent;
continue;
}else{
/**
* 叔叔节点是黑色
* 判定当前节点是左子节点还是右子节点
* 左子节点先右旋后左旋;右子节点先左旋后右旋。
*
* 为什么左旋完就直接右旋?
* 因为左旋转只是 当前节点和父节点变换位置(父变为子,子变为父)。颜色没有边。原先叔叔节点也没动。旋转后只是
* 当前元素变成了左子节点,完全符合右旋的条件。再以父节点。同理右旋的情况。
*/
//当前节点是右子节点
if(node == node.parent.right){
//父节点进行左旋
node = node.parent;
rotateLeft(node);
//左旋完后 父节点变为黑色,祖父节点变为红色
node.parent.color = BLACK;
node.parent.parent.color = RED;
rotateRight(node.parent.parent);
}else{
//父节点进行右旋
node = node.parent;
rotateRight(node);
//左旋完后 父节点变为黑色,祖父节点变为红色
node.parent.color = BLACK;
node.parent.parent.color = RED;
rotateLeft(node.parent.parent);
}
}
}
root.color = BLACK;
}
删除
没必要了吧,看不下去了。删除也可能会打破平衡,需要旋转和变色来平衡。
查找
与普通的二叉查找树类似,通过比较节点的键值来查找目标节点。查找操作的时间复杂度为O(log n),因为红黑树的高度始终保持在log n级别。
红黑树广泛应用于各种计算机系统中,如java中的map,红黑树通过旋转和颜色调整来保持平衡,避免了树的高度过大,确保了高效的操作。在最坏情况下,红黑树的查找、插入和删除操作的时间复杂度都是O(log n)。