在介绍红黑树之前我们先介绍一下BST树和AVL树:
BST树又叫做二叉搜索树;该树的特点是:树中的数值不能重复;任意结点的左孩子的值比该结点的值小;任意结点的右孩子的值比该结点的值大。这样的树就叫做BST树,图示如下:
这就是一颗BST树,此结构拥有二叉树结构的优点,理论上能到达logn的插入和删除效率,但是很不幸,此结构在存储单调变化的数据是会退化为链表;当我们插入单递增的数据{12,34,45,56,67,78,89}时,BST树就退化为如下的链表:
这样的话就不具备二叉树的 logn的增删改查效率 ,处理这个问题我们引入了 AVLTree
AVLTree是一个可自平衡的二叉搜索树,它满足BST树的所有性质,但是它额外要求树中任意结点的左右树高只差小于等于 1,这样的话在存储单调变化的数据时,整个结构就可以拥有二叉树的logn的效率。它的每一个结点都多了一个成员:balance,该成员的取值为-1,0,1三者中的一个,balance值的确定是依靠 Depth(root->right) - Depth(root->left) 的结果。假设我现在插入的数据还是{12,34,45,56,67,78,89},AVLTree的存储结果如下:
该树的生成过程可在:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html 查看,当然也可以测试其他的数据来看AVLTree的平横过程。
在这里讲一下树的旋转:(要能看懂,之前需要对旋转有一点了解)
旋转的目的是为了降低树的高度,其分为"单旋"和"双旋",旋转整体的模式图为:
接下来以一个旋转的实例来进行说明:
struct AVLnode
{
int val;
struct AVLnode* left;
struct AVLnode* right;
struct AVLnode* parent;
}
//下述代码阅读建议:将三个修改父指针的代码去掉后再查看逻辑,修改父指针只是为了旋转后维持树的结构
void LeftRotate(AVLnode *ptr) //左旋代码;右旋的实现是一个镜像的过程
{
AVLnode *newroot = ptr->right; //保存左旋后替代ptr的结点newroot
newroot->parent = ptr->parent; //第1个修改父指针
ptr->right = newroot->left; //保存之后,让ptr->right指向newroot的内侧节点
newroot->left->parent = ptr; //第2个修改父指针
newroot->left = ptr; //newroot的内测结点位置被换为ptr
AVLnode *pa = ptr->parent;
if(ptr == pa->left) //newroot替换原来ptr在树中的位置
pa->left = newroot;
else
pa->right = newroot;
ptr->parent = newroot; //第3个修改父指针
}
说明:
代码的实现一定要结合自己所写的树的结构来写参数列表;上述
代码完成了旋转前后的父子指针的修改,逻辑完全正确!但是为
了保证代码的易读性没有将其他的内容参杂进来,读者在利用的
时候要结合自己树的结构进行一定的修补。
接下来让我们进入到红黑树的插入模块的介绍:
在介绍插入的时候,我们我们要知道红黑树是一个BST树,其次对红黑树的5大性质一定要了解,5大性质如下:
1. 每个节点或是红的或是黑的
2. 根节点为黑色
3. 外部节点是黑色的
4. 如果一个结点是红色的,那么它的两个儿子都是黑色的 (红红不能相连)
5. 对每个结点的,从该节点出发到外部结点的所有路径上包含相同个数的黑色结点(黑高相同)
因此我们在插入结点时,要让待插入节点的颜色为红色,然后直接插入,这样插入一个红色结点后不会影响性质5,但是有可能使性质4无法满足,所以插入后我们需要进行调整,调整策略如下(记插入的结点为X):
(1)X的父节点为黑色,不用调整,满足红黑树的性质!直接退出调整函数。原因如图:
(2)X的父节点为红色,此时不满足性质4,需要进行调整(记X的父节点为 pa):
(2.1)pa为红色,pa的兄弟节点也为红色,展示如下:
此情况下的处理是将pa和pa的兄弟结点的颜色都变为黑色,X=pa->parent ;
这么做的后果:1. 从pa->parent到外外部节点的黑高不变;
2. 但是pa->parent变为红色后,由于我们不知道pa->parent->parent的颜色是红是黑,所以我们以 pa->parent当作新的X再次进入整个调整函数
3. 这个X有可能会连续出现多次上移
4. 当我们的新的X为根节点时,根节点的颜色已经变成红色不满足“跟必须为黑色”的性质,需要将 跟变黑
(2.2)不满足2.1的话,那么肯定目前的情况肯定就是:pa为红色,pa的兄弟节点为黑色
(2.2.1) X是其爷爷的外部孙子,模式如下:
模式说明:未插入X前整个树是红黑树,pa为红色,则pa->parent必定为黑色,所以该模式的一定为 图示的结构!
该模式的变化方式为下(仅以X为其爷爷结点的右侧孙子为例;但X为其爷爷的左侧孙子为镜像操作):
变换后的后果:符合红黑树中所有的性质,结束调整!
(2.2.2) X是其爷爷的内部孙子,模式如下:
模式说明:未插入X前整个树是红黑树,pa为红色,则pa->parent必定为黑色,所以该模式的一定为 图示的结构!
该模式的变化方式为下(仅以X为其爷爷结点的右侧孙子为例;但X为其爷爷的左侧孙子为镜像操作):
变换后的后果:符合红黑树中所有的性质,结束调整!
总结:
插入的结点都是红色!
每次调整结束都要保证跟为黑色!
整个插入的调整最多执行一个单旋或者一个双旋就可以完成调整!
伪代码:
上述的2.1对应的case1
2.2.1对应的是case2
2.2.2对应的是case3
翻译代码如下:
typedef struct rb_node
{
int val;
int color;
struct rb_node *parent;
struct rb_node *leftchild;
struct rb_node *rightchild;
}rb_node;
void Adjust_RBTree(rb_tree &myt, rb_node *ptr)
{
rb_node *_X = ptr;
rb_node *_Y = NULL;
for (; _X->parent != myt.head && _X->parent->color == RED;) //当父节点是红色,或者回溯到跟 时,就不必在继续了
{
if (_X->parent == _X->parent->parent->rightchild) //即在爷爷结点的右端插入
{
_Y = _X->parent->parent->leftchild; //表_X的叔父
if (_Y->color == RED)
{
_X->parent->color = BLACK;
_Y->color = BLACK;
_X->parent->parent->color = RED;
_X = _X->parent->parent;
}
else
{
if (_X == _X->parent->leftchild) //内节点,进行双旋转第一转
{
_X = _X->parent; //旋转一次 将_X位置变化 就适用于下面的几行代码
RotateRight(myt, _X);
}
_X->parent->color = BLACK;
_X->parent->parent->color = RED;
RotateLeft(myt, _X->parent->parent); //外结点,则单旋转
}
}
else
{
_Y = _X->parent->parent->rightchild;
if (_Y->color == RED)
{
_Y->color = BLACK;
_X->parent->color = BLACK;;
_X->parent->parent->color = RED;
_X = _X->parent->parent;
}
else
{
if (_X = _X->parent->rightchild)
{
_X = _X->parent;
RotateLeft(myt, _X);
}
_X->parent->color = BLACK;
_X->parent->parent->color = RED;
RotateRight(myt, _X->parent->parent);
}
}
}
myt.head->parent->color = BLACK; //根节点无论此时啥颜色直接置为黑色
}