引言
红黑树作为一种面试常被提问的数据结构,自己应该弄懂其原理和树结构的变化规则,给自己不留坑。
问题:
- 红黑树的特性
- 红黑树的时间复杂度
- 红黑树与二叉搜索树的区别
- 红黑树的颜色变化规则
- 红黑树的左旋和右旋操作及其左旋和右旋时机
- 红黑树目前的应用场景有哪些?性能优缺点
- 红黑树的节点默认设置为红色和默认设置为黑色区别?TreeMap的默认设置颜色?
红黑树的特性
1.红黑树的所有节点不是红色就是黑色
2.红黑树的所有叶子节点为黑色(叶子节点指NULL节点)
3.根节点为黑色
4.任意一个红节点的两个子节点不能为红色
5.从任意一个节点出发,到齐叶子节点的所有路径上包含的黑色节点数相同
左旋和右旋操作过程
(图片来源于网络)
本人理解红黑树结构就是从这两张动图开始的
红黑树的代码实现网上很多,但是建议参照TreeMap的源码,亲自动手实现,找出差异。
代码实现
public class RebBlackTree<T extends Comparable<T>> {
//根节点
private Node<T> root;
private static final boolean RED = false;
private static final boolean BLACK = true;
static class Node<T extends Comparable<T>> {
boolean color;
T key;
Node<T> left;
Node<T> right;
Node<T> par
ent;
Node(T key) {
this.key = key;
//默认节点为红色
this.color = RED;
//父节点在插入的过程中会进行重新设置
this.parent = null;
this.left = null;
this.right = null;
}
}
/**
* 红黑树的插入与二叉查找树的插入相同,在元素插入之后需要设置元素的父节点
* 节点默认为红色
* 插入之后进行红黑树的修正
* 然后进行左旋和右旋操作
* @param node
*/
public void insert(Node<T> node) {
int cmp;
Node<T> x = this.root;
Node<T> y = null;//父节点
//寻找插入的父节点
while (x != null) {
y = x;
cmp = node.key.compareTo(x.key);
if (cmp < 0) {
x = x.left;
} else {
x = x.right;
}
}
//设置插入的父节点
node.parent = y;
/**
* 判断插入到父节点的左边还是右边
*/
if (y != null) {
cmp = node.key.compareTo(y.key);
if (cmp < 0) {
y.left = node;
} else {
y.right = node;
}
} else {
//如果父节点为空则此节点为根节点
this.root = node;
}
/*
从新插入的节点开始修正红黑树
此处应该可以理解红黑树的是局部平衡
*/
insertFixUp(node);
}
/**
* 插入节点之后修正红黑树
* 修正红黑树的过程为从下往上
* 修正的过程中轨进行颜色变化和左旋以及右旋操作
* @param node
*/
private void insertFixUp(Node node) {
//父节点
Node<T> parent;
//爷爷节点
Node<T> gparent;
/**
* 如果父节点不为空,并且父节点为红色
*/
while (((parent = parentof(node)) != null) && isRed(parentof(node))) {
gparent = parentof(parent);
//如果父节点为左孩子
if (parent == gparent.left) {
Node<T> uncle = gparent.right;
//case1:父节点为红色,叔叔的节点为红色
if ((uncle != null) && isRed(uncle)) {
/**
* 设置爷爷节点为红色
* 设置父节点和叔叔节点为黑色
* 当前节点到爷爷节点
*/
setRed(gparent);
setBlack(parent);
setBlack(uncle);
node = gparent;
continue;
}
//case2:当前节点为右孩子节点,父节点为红色,叔叔节点为黑色
if (node == parent.right) {
/**
* 左旋操作
*/
node = parent;
leftRotate(parent);
}
//case3:当前节点为左孩子节点,父节点为红色,叔叔节点为黑色,
/**
* 右旋操作
* 父节点变为黑色,爷爷节点变为红色
*/
rightRotate(gparent);
setRed(gparent);
setBlack(parent);
}
//如果父节点为右孩子
else {
Node<T> uncle = gparent.left;
//case1:当前节点为红色,叔叔节点为红色
if ((uncle != null) && isRed(uncle)) {
setBlack(uncle);
setBlack(parent);
setRed(gparent);
node = gparent;
continue;
}
// Case 2条件:叔叔是黑色,且当前节点是左孩子
if (parent.left == node) {
/**
* 右旋
*/
node = parent;
rightRotate(parent);
}
// Case 3条件:叔叔是黑色,且当前节点是右孩子。
/**
* 左旋
*/
setBlack(parent);
setRed(gparent);
leftRotate(gparent);
}
}
/**
* 如果父节点为空则此节点为根节点
* 设置为黑色
*/
setBlack(root);
}
/**
* 以node节点进行左旋操作
*
* @param node
*/
private void leftRotate(Node<T> node) {
//x节点的右节点
Node<T> x = node.right;
//x的右节点为y节点的左节点
node.right = x.left;
node.parent = x;
x.left.parent = node;
x.parent = node.parent;
x.left = node;
//设置y节点的父节点
if (node.parent != null) {
if (node == node.parent.right) {
node.parent.right = x;
} else {
node.parent.left = x;
}
} else {
this.root = x;
}
}
/**
* 以node节点进行右旋操作
* @param node
*/
private void rightRotate(Node<T> node) {
Node<T> x = node.left;
x.parent = node.parent;
x.right.parent = node;
node.left = x.right;
node.parent = x;
if (node.parent != null) {
//如果y节点本来为右节点
//则父节点的右节点为x
if (node == node.parent.right) {
node.parent.right = x;
} else {
node.parent.left = x;
}
} else {
this.root = x;
}
}
//返回父节点
private Node<T> parentof(Node<T> node) {
return node != null ? node.parent : null;
}
//是否为红色节点
private boolean isRed(Node<T> node) {
return (node != null && node.color == RED) ? true : false;
}
//是否为黑色节点
private boolean isBlack(Node<T> node) {
return !isRed(node);
}
//设置黑色
private void setBlack(Node<T> node) {
if (node != null) {
node.color = BLACK;
}
}
//设置红色
private void setRed(Node<T> node) {
if (node != null) {
node.color = RED;
}
}
}
规律
- 当前节点为左节点,必为右旋;当前节点为右节点必为左旋
- 左左(父节点和当前节点都为左节点),直接右旋;右右(父节点和当前节点都为右节点),直接左旋;旋转点为爷爷节点
- 最后一次左旋或者右旋后,设置父亲节点为黑色,爷爷节点为红色
- 左右操作和右左操作中,当前节点移动到父节点
总结
-
红黑树颜色默认为红色
如果颜色默认设置为黑色,容易破坏性质5,则每次插入元素之后都要进行左旋或者右旋操作,影响性能
-
应用场景
HashMap中链表会进行树化,树化时为红黑树