一.二叉搜索树
二叉树是最常用的树形数据结构,二叉树可以分为完全二叉树,满二叉树,平衡二叉树。二叉树应用的最多就是二叉搜索树,二叉搜索树的定义是:设x是二叉搜索树中的一个结点。如果y是x的左子树中的一个结点,那么y.key<=x.key。如果y是x右子树中的一个结点,那么y.key>=x.key。 也就是左子树小于根节点,根节点小于右子树。
普通的二叉搜索树效率很低,比如按照从小到大的顺序插入数值,二叉搜索树就会退化成链表:
这样的搜索效率和链表一样,复杂度是O(n)。
二.平衡二叉搜索树
二叉搜索树最理想的结构就是在每一层有尽可能多的结点,使得二叉树高度降低,那最好的结构就是完全二叉树结构,其中满二叉树每层结点数2^n (n >=0), 全部节点个数为2^n 1 - 1。从上到下从左到右依次排满。这样我们的搜索效率时间复杂度就会降低到O(lgn)。所以就需要设计一种平衡二叉树的数据结构。AVL树就是一种很好的平衡二叉树,AVL树规定每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1,这样AVL树树的高度就和完全二叉树一样了,但是AVL树在工业界使用并不多。
红黑树也是一种平衡二叉树,红黑树应用广泛,linux内核的rbtree.c,jdk的TreeMap和HashMap等都实现了红黑树结构。写红黑树的性质前,先按照自己的思维去考虑,红黑树为什么这样定义。平衡二叉树的设计目标就是在结构上要接近完全二叉树,AVL树就是高度平衡的二叉树,树的高度符合完全二叉树的结构。思考:
1.如果从根结点到所有叶子结点的结点个数相等,那平衡度最好,比AVL还好,AVL还允许高度相差为1,但是这种形态是一个满二叉树,实现不了,插入删除的过程中不可能高度都一样。那就需要加入第二个限制条件。
2.AVL树限制高度差不能超过1。那我们就使用颜色产生高度差,让黑色结点满足上一条性质,也就是从根结点到叶子结点的黑色结点个数相等,红色结点个数不做限制,这样产生了高度差,并且两条简单路径的红色结点之差就是两条路径的高度差,但是又会产生新问题,如果不对红色结点做限制,那就会退化成了普通二叉树了,比如1个黑结点n个红结点又变成了O(n)的复杂度。所以还需要对红色结点做限制。
3.如何对高度差做限制,红黑树定义了,如果当前结点是红色,则其孩子结点必须为黑色,也就是父子结点不能同时为黑色,一条路径上的红结点要小于等于黑色结点(null不算)。设红黑树高度为h,则高度差最大为h/2,因此从根结点到叶子结点最短路径是h/2。
通过以上思考可以大致了解红黑树的设计意图,平衡二叉树就是要接近完全二叉树的结构,AVL树通过平衡因子控制高度差不超过1,红黑树通过红色和黑色结点的限制控制高度差,使其不超过h/2。
三.红黑树定义
1.每个节点或者是红色的,或者是黑色的。
2.根节点是黑色的。
3.每个叶子结点(null结点)是黑色的。
4.如果一个结点是红色的,则它的孩子结点必须是黑色的。
5.对于每一个结点,到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点。
设红黑树高度为h,则红黑树到叶子结点最短路径(最小高度)大于等于h/2(根据性质45可得)。红黑树的增删改查时间复杂度都是O(lgn)。
一颗含有n个结点的红黑树,最大高度为2log(n 1)。相关证明可以参考算法导论。
四.二叉搜索树的旋转
二叉搜索树分为左旋和右旋,旋转的最终目的就是使得树的高度降低。比如:
节点1通过左旋,使得二叉树的高度从3降到了2,而且不会失去二叉搜索树的性质,即左子树小于根节点小于右子树。
左旋:设x为当前结点,y为x的右结点,β为y的左节点,P为x的父结点,则x以y为支点进行左旋转,旋转后,P指向y, x的右结点指向β, y的左结点指向x。
右旋:设y为当前结点,x为y的左结点,β为x的右结点,P为y的父结点,则y以x为支点进行右旋转,旋转后,P指向x,y的左结点指向β,x的右结点指向y。
五.红黑树的插入操作
红黑树是二叉搜索树的一种,所以它的插入首先要遵守二叉搜索树的性质,从根结点开始,如果待插入结点的值大于当前结点则向右子树遍历,否则像左子树遍历,直到遇见null结点位置,然后将待插入结点插入。
红黑树插入步骤:
1.将红黑树按照二叉搜索树的方式插入。
2.将插入的结点设置为红色(插入的结点设置为红色,只可能违反性质4。如果插入的结点设置为黑色,一定会违反性质5。单从违反红黑树的性质来说,插入红色违反的概率小于插入黑色违反的概率,黑色100%违反。并且插入黑色的话,当前路径的黑色结点和其它所有路径的黑色结点个数都不一样了这样调整起来相对于插入的结点是红色也麻烦)。
3.对插入的新结点进行调整。
红黑树调整过程:
1.如果当前结点是根结点,直接将结点设置为黑色。
2.如果当前结点的父结点是黑色,什么也不需要做。
3.如果当前结点的父结点是红色,则违反性质4,需要进行调整,主要分以下三种情况:
(1).当前结点的父结点是红色,并且当前结点的叔叔结点也是红色:将父结点设置为黑色;将叔叔结点设置为黑色;将祖父结点设置为红色;将当前结点指向祖父节点,在下一个循环中继续对当前结点进行操作。
解释:父结点是红色则祖父结点一定是黑色,将祖父结点变成红色,父结点和叔叔结点编程黑色,则以祖父结点为根的子树恰好符合红黑树性质。下面就需要以祖父结点为当前结点,在下一轮循环中,判断祖父结点是否违反性质四,然后继续调整。
插入结点6,变化如下图
(2).当前结点的父结点是红色,当前结点的叔叔结点是黑色,并且当前结点是父结点的右孩子:将父结点做为当前结点;然后将当前结点进行左旋。
解释:此种形态不能一步将以祖父节点为根的子树变成符合红黑树性质的子树,所以需要将这种情况转换为情况3。
插入结点9,则变化如下图
(3).当前结点的父结点是红色,当前结点的叔叔结点是黑色,并且当前结点是父结点的左孩子:将父结点设置为黑色;将祖父结点设置为红色;将祖父结点进行右旋。
解释:这种形态经过一次变换就可以满足红黑树的性质,首先将父结点设置为黑色满足了性质四,但是违反了性质5,然后将祖父结点设置为红色,通样则父结点这个分支满足了红黑树性质,但是叔叔结点的分支违反了性质5。所以需要再将祖父结点右旋,这样全部都满足了红黑树性质。
由情况二变为情况三,然后情况三变化如下
总结:红黑树的插入在违反规则4的时候有三种情况,其中情况1情况3都可以一次旋转是当前子树符合红黑树性质,并且情况3使得整个红黑树都符合性质,情况一则需要向上回溯判断是否符合性质。情况二需要两次变化先变化为情况三,然后情况三再转化为符合红黑树性质的树。
六.红黑树删除操作
红黑树删除操作首先也要按照二叉搜索树的方式去删除。普通二叉搜索树删除:从根结点开始遍历,如果要删除的值大于当前结点则向右遍历,否则向左遍历,找到要删除的结点。删除分三种情况:
1.如果待删除结点没有孩子结点则直接删除;
2.如果有一个孩子结点则将孩子结点顶替被删除的结点;
3.如果有两个孩子结点则找当前结点的后继结点(当前结点左子树的最大节点或者右子树的最小结点)然后将后继结点的值复制给当前结点,然后按照1或者2的方式删除后继结点。
删除完成后需要对红黑树做调整,主要分以下情况:
1.被删结点是红色,则直接删除,不会违反红黑树的5条性质。
2.被删结点是黑色,并且不是根结点,一定会违反性质5(少了一个黑结点),也可能违反性质4(顶替的结点是红色,可能和父节点冲突,也可能顶替到了根结点违反性质2)。这种情况需要做调整,整个调整都是依据顶替结点分析的。具体调整还可在细分以下7种情况,顶替结点称为当前结点:
(1).当前结点是红色:直接设置为黑色。
解释:由于删了一个黑结点则会导致违反性质2或者性质5,但是顶替的结点是红色,则直接将红色变黑色就恢复了红黑树的性质。
(2).当前结点是黑色,且当前结点是根结点:什么都不做直接退出。
解释:根结点是黑色结点,不违反红黑树性质。
(3).当前结点是黑色,且当前结点的兄弟结点是红色:将当前结点的父节点设置为红色;将当前结点的兄弟结点设置为黑色;将父节点左旋;重新设置当前结点的兄弟结点。
解释:这种情况无法一次转为符合性质的红黑树,需要转换为以下三种情况的任意一种。
调整结点6。
(4).当前结点是黑色,当前结点的兄弟结点是黑色,并且兄弟节点的两个孩子也是黑色:将当前结点的兄弟结点设置为红色;将当前结点指向它的父结点(父结点颜色未知)。
解释:记当前结点到叶子结点的黑色结点个数是c(x),则c(15) = c(6) 1(被删是黑结点)。而此时15的两个孩子都是黑色,所以此时不能通过变换结点颜色或者旋转使得以c(6)为根结点的子树黑色结点加一,此时需要向根节点走,所以将兄弟结点设置为红色,使得c(15) = c(6),也就是以10为根结点的子树所有黑结点减一。c(10) = c(10) - 1。然后在下次调整中以10为结点进行调整。这种情况和红黑树插入中的第一种情况类似,红黑树的插入只有第一种情况可能会让整个红黑树各个路径的黑结点个数加1。同样删除中的这种情况,也可能会让整个红黑树各个路径的黑结点个数减一。
(5).当前结点是黑色,当前结点的兄弟结点是黑色,兄弟结点的左孩子是红色右孩子是黑色:将兄弟结点的左孩子设置为黑色;将兄弟结点设置为红色;将兄弟结点进行右旋转;重新设置当前结点的兄弟结点。
解释:这种情况和红黑树插入情况中的第二种情况一样,都需要转换为最后一种情况,因为最后一种情况可以通过一次变换达到符合红黑树性质。
(6).当前结点是黑色,当前结点的兄弟结点是黑色,兄弟结点的右孩子是红色,左孩子是任意颜色:将当前结点父结点的颜色赋给当前结点的兄弟结点;将父结点设置为黑色;将兄弟结点的右结点设置为黑色;对父结点进行左旋;设置当前结点为根结点。
解释:这种情况和红黑树插入的第三种情况类似,都是通过一次操作就可以使红黑树重新符合性质。这个操作的目标是为了让c(6的父结点)恢复删除前黑色结点的个数,将父结点颜色赋给兄弟结点是为了旋转做准备(让兄弟结点做父结点),将父结点设置为黑色这样旋转后结点6这一分支黑色结点就恢复了删除前的个数。将兄弟结点的右结点设置为黑色,是因为旋转后12的这一分支会少一个黑结点。可以用公式做下简单的证明:如果10为黑色(红色也可证明)则c(10) = c(6) 2 = c(12) 1。c(12) = c(6) 1。所以为了达到平衡,结点6这一支需要多一个黑结点,而12那一支需要不变。将10的颜色给12,12置为黑色,无影响。将兄弟结点的右结点置为黑色,则c(12) = c(12) 1;多了一个黑结点,此时c(12) - c(6) = 2。所以需要将12这一支的黑结点给6这一支。这样正好达到平衡。所以需要将10左旋。旋转后以12为根结点的左右子树黑色结点个数就会相等,使得红黑树重新平衡。
总结:红黑树的删除调整和插入调整很类似。以上的(3)(4)(5)(6)可以说是删除过程中主要做调整的四种情况。将(3)(4)(5)(6)从分别叫做删除过程中需要调整的第一第二第三第四种情况。其中第二第三第四和插入过程中的第一第二第三种情况很类似。
1.删除第二种情况和插入的第一种情况类似,删除第二种情况需要向上进行循环调整并且可能会导致整个树的每条路径黑结点个数少1个,插入第一种情况也需要向上进行循环调整并且可能会使整个树的每条路径黑结点个数多1个。
2.删除第三种情况和插入第二种情况也类似,这种情况下树的形态都不能一次性转化为满足红黑树性质的形态,都需要做一次中转,变成可以通过一次操作满足红黑树性质的形态。
3.删除的第四种情况和插入的第三种情况类似,这种形态的树都可以通过一次操作使其符合红黑色的性质,然后退出调整。
4.所以我们就需要将删除的第一种情况转换为其余的三种情况。
以上大致分析完了红黑树的插入和删除。
七.红黑树实现 java版
代码语言:javascript复制package pers.pzt.seckill;
public class RBTree<T extends Comparable<T>> {
//树结点定义
private class TreeNode {
T key;
boolean color; //true为黑色,false为红色
TreeNode parent; //当前节点的父节点
TreeNode left; //当前节点的左孩子
TreeNode right; //当前节点的右孩子
//生成的节点默认为红色
private TreeNode(T key) {
this.key = key;
this.color = false;
}
}
private TreeNode root; //红黑树根节点
private int size; //红黑树结点个数
/**
* node节点左旋
* @param node,要旋转的节点
*/
private void leftRotate(TreeNode node) {
if(node != null && node.right != null) {
TreeNode parent = node.parent;
TreeNode rightNode = node.right;
node.right = rightNode.left;
if(rightNode.left != null)
rightNode.left.parent = node;
rightNode.left = node;
node.parent = rightNode;
if(parent == null)
root = rightNode;
else if(parent.left == node)
parent.left = rightNode;
else
parent.right = rightNode;
rightNode.parent = parent;
}
}
/**
* node节点右旋
* @param node 要旋转的节点
*/
private void rightRotate(TreeNode node) {
if(node != null && node.left != null) {
TreeNode parent = node.parent; //要旋转节点的父节点
TreeNode leftNode = node.left; //要旋转节点的支点
node.left = leftNode.right; //将支点的右孩子给旋转节点的左孩子
if(leftNode.right != null) //如果支点的右孩子不为null
leftNode.right.parent = node; //将支点的右孩子的父亲指向node
leftNode.right = node; //支点的右孩子指向node
node.parent = leftNode;
if(parent == null) //如果要旋转节点的父节点为null说明要旋转的节点是根节点
root = leftNode;
else if(parent.left == node) //否则将父节点的左孩子或者右孩子指向支点
parent.left = leftNode;
else
parent.right = leftNode;
leftNode.parent = parent; //将支点的父亲指向parent
}
}
/**
* 根据红黑树性质,对刚插入的node节点做调整
* @param node
*/
private void fixup(TreeNode node) {
TreeNode parent = node.parent;
//循环终止条件是当前节点的父结点不为null,并且父结点的颜色是黑色,保证相邻的父子节点不能同时为红色,并且
//从根节点到叶子节点的黑色节点个数相等。
while(parent != null && !parent.color) {
TreeNode grandParent = parent.parent; //祖父节点
if(parent == grandParent.left) { //如果当前节点的父结点是祖父节点的左节点
TreeNode uncle = grandParent.right; //叔叔节点
if(uncle != null && !uncle.color) { //如果叔叔节点是红色
parent.color = true;
uncle.color = true;
grandParent.color = false;
node = grandParent;
parent = node.parent;
} else { //如果叔叔节点是黑色,分两种情况
if(parent.right == node) {//如果当前节点是右子树
leftRotate(parent); //左旋转,变成第三种情况
node = parent;
parent = node.parent;
}
grandParent.color = false; //当前节点是左子树
parent.color = true;
rightRotate(grandParent);
}
} else { //如果当前节点的父结点是祖父节点的右节点
TreeNode uncle = grandParent.left; //叔叔节点
if(uncle != null && !uncle.color) { //如果叔叔节点是红色
parent.color = true;
uncle.color = true;
grandParent.color = false;
node = grandParent;
parent = node.parent;
} else { //如果叔叔节点不是红色,分两种情况
if(parent.left == node) {
rightRotate(parent);
node = parent;
parent = node.parent;
}
grandParent.color = false;
parent.color = true;
leftRotate(grandParent);
}
}
}
root.color = true; //设置根节点为黑色
}
/**
* 如果node左右孩子都有,则需要找后继结点,查找node右子树最小节点做为后继结点
* @param node
* @return
*/
private TreeNode findNextNode(TreeNode node) {
TreeNode rightNode = node.right;
while(rightNode.left != null)
rightNode = rightNode.left;
return rightNode;
}
/**
* 调整新插入的node
* @param repNode
*/
private void deleteFixup(TreeNode node) {
while(node != root && node.color == true) {
TreeNode brother = null;
if(node == node.parent.left) {
brother = node.parent.right;
if(brother != null && !brother.color) {//兄弟节点颜色是红色
brother.color = true;
brother.parent.color = false;
leftRotate(brother.parent);
}
//如果兄弟节点是黑色
if(brother == null || brother.color == true) {
//如果兄弟节点的2个孩子都是黑色
if((brother.left == null || brother.left.color == true) &&
(brother.right == null || brother.right.color == true)) {
brother.color = false;
node = node.parent;
} else {
if((brother.left != null && brother.left.color == false) &&
(brother.right == null || brother.right.color == true)) {
brother.left.color = true;
brother.color = false;
rightRotate(brother);
brother = node.parent.right;
}
brother.color = node.parent.color;
node.parent.color = true;
brother.right.color = true;
leftRotate(node.parent);
node = root;
}
}
} else {
brother = node.parent.left;
if(brother != null && !brother.color) {//兄弟节点颜色是红色
brother.color = true;
brother.parent.color = false;
rightRotate(brother.parent);
}
//如果兄弟节点是黑色
if(brother == null || brother.color == true) {
//如果兄弟节点的2个孩子都是黑色
if((brother.left == null || brother.left.color == true) &&
(brother.right == null || brother.right.color == true)) {
brother.color = false;
node = node.parent;
} else {
if((brother.right != null && brother.right.color == false) &&
(brother.left == null || brother.left.color == true)) {
brother.right.color = true;
brother.color = false;
leftRotate(brother);
brother = node.parent.left;
}
brother.color = node.parent.color;
node.parent.color = true;
brother.left.color = true;
rightRotate(node.parent);
node = root;
}
}
}
}
node.color = true;
}
private void deleteNode(TreeNode tmp) {
TreeNode delNode = null; //待删除节点
TreeNode repNode = null; //替换待删除节点
//查找待删除节点,如果左右孩子都不为null则查找tmp的后继节点,做为待删除节点
if(tmp.right == null || tmp.left == null)
delNode = tmp;
else
delNode = findNextNode(tmp);
if(delNode.left != null)
repNode = delNode.left;
else
repNode = delNode.right;
//将替换节点的父亲指向待删除节点的父节点,如果替换节点为空则说明删除的节点是尾结点
if(repNode != null)
repNode.parent = delNode.parent;
if(delNode.parent == null)
root = repNode;
else if(delNode.parent.left == delNode)
delNode.parent.left = repNode;
else
delNode.parent.right = repNode;
if(delNode != tmp)
tmp.key = delNode.key;
if(delNode.color == true && repNode != null)
deleteFixup(repNode);
}
public void insert(T key) {
TreeNode newNode = new TreeNode(key);
if(root == null) {
root = newNode;
root.color = true;
} else {
TreeNode tmp = root;
int cmp = 0;
while(true) {
if((cmp = key.compareTo(tmp.key)) < 0 && tmp.left != null)
tmp = tmp.left;
else if((cmp = key.compareTo(tmp.key)) > 0 && tmp.right != null)
tmp = tmp.right;
else
break;
}
if(cmp == 0)
return;
else if(cmp < 0)
tmp.left = newNode;
else
tmp.right = newNode;
newNode.parent = tmp;
fixup(newNode);
}
size ;
}
public void delete(T key) {
TreeNode tmp = root;
while(tmp != null) {
if(key.compareTo(tmp.key) > 0)
tmp = tmp.right;
else if(key.compareTo(tmp.key) < 0)
tmp = tmp.left;
else
break;
}
if(tmp != null) {
deleteNode(tmp);
size--;
}
}
//获取红黑树节点个数
public int size() {
return this.size;
}
/**
* 求红黑树的高度
* @param node
* @return
*/
public int getDepth(TreeNode node) {
return node == null ? 0 : 1 Math.max(getDepth(node.left), getDepth(node.right));
}
public static void main(String[] args) {
RBTree<Integer> tree = new RBTree<>();
long startTime = System.currentTimeMillis();
for(int i = 1; i < 100000; i )
tree.insert(i);
long endTime = System.currentTimeMillis();
System.out.println("insert spend time: " (endTime - startTime) "ms");
System.out.println("rb tree depth: " tree.getDepth(tree.root));
startTime = System.currentTimeMillis();
for(int i = 1; i < 100000; i )
tree.delete(i);
endTime = System.currentTimeMillis();
System.out.println("delete spend time: " (endTime - startTime) "ms");
}
}