引言
首先为什么要有红黑树,红黑树是在AVL树也就是平衡二叉树后出现的,AVL树严格的规定的子树的高度差不能超过1,所以当遇到频繁的插入删除操作时,调整树会比较费时间(PS:也仅仅是一点点费时间,其实AVL树已经很优秀了)。而红黑树的规则没这么严格,在保证查找效率与AVL数一个数量级,也就是log(n)的情况下,插入删除操作的效率也提高了许多,STL里面可排序集合set的内部实现用的就是红黑树。
红黑树的特点
- 首先红黑树是一棵二叉查找树,具有二叉查找树的所有特性。
- 红黑树的节点是红色或黑色。
- 根节点是黑色。
- 每个叶子节点都是黑色的空节点(NIL节点)。
- 每个红色节点的两个子节点都是黑色,顾名思义,从每个叶子到根的所有路径上不能有两个连续的红色节点。
- 从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点,也有人说黑高是相同的。
下面这张图就是典型的红黑树
由于红黑树本身就是一棵二叉查找树,所以其插入删除操作是与普通二叉查找树一样的,只不过插入删除完成后有可能会破坏红黑树的规则,所以需要额外的修复操作。
预备知识
红黑树是计算机大牛研究出来的非常优秀的数据结构,我们当然没有必要再现其心路历程,只需熟练掌握这一套框架即可。
上面也提到了在红黑树的插入删除完成后需要额外的修复操作,修复操作就包括两部分:
- 变色,很简单,黑变红,红变黑即可。
- 旋转,旋转又分为左旋和右旋:
-
左旋
- 旧根节点为新根节点的左子树
- 新根节点的左子树(如果存在)为旧根节点的右子树
-
右旋
- 旧根节点为新根节点的右子树
- 新根节点的右子树(如果存在)为旧根节点的左子树
-
温习二叉查找树的常规操作
- 删除一个节点
力扣450题:删除二叉搜索树中的节点
节点定义及函数实现为:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
//如果为叶子节点,直接删除
//如果只有一个孩子,交换与孩子节点的值,并将孩子节点的左子树变为自己的左子树,将孩子节点的右子树变为自己的右子树。
//如果有两个孩子,则需要找到前驱或后继节点,交换之,然后变为上述两种情况,比如示例1
//前驱节点:左孩子的最右边,如果孩子没有右子树则返回本身
//后继节点:右孩子的最左边,如果孩子没有左子树则返回本身
//先试图找到该节点
TreeNode* head = new TreeNode(1e6);
head->left = root;
TreeNode* parent = head,*target = nullptr,*t = root;
bool flag = false;
while(true){
if(t==nullptr) break;
if(t->val==key){
flag = true;target = t;break;
}
parent = t;
if(t->val>key) t = t->left;
else t = t->right;
}
if(!flag) return root;
while(true){
if(target->left==nullptr&&target->right==nullptr){
if(parent->left==target) parent->left = nullptr;
else parent->right = nullptr;
delete target;
break;
}
else if(target->left==nullptr&&target->right!=nullptr){
TreeNode *p = target->right;
target->val = p->val;
target->left = p->left;
target->right = p->right;
delete p;
break;
}
else if(target->left!=nullptr&&target->right==nullptr){
TreeNode *p = target->left;
target->val = p->val;
target->left = p->left;
target->right = p->right;
delete p;
break;
}
else{
TreeNode* pre = target->left;
parent = target;
while(pre->right!=nullptr){
parent = pre;
pre = pre->right;
}
swap(target->val,pre->val);
//pre可能有左孩子,但肯定没有右孩子
target = pre;
}
}
return head->left;
}
};
- 插入一个节点
力扣701题:二叉搜索树中的插入操作
节点定义及函数实现为:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if(root==nullptr) {
root = new TreeNode(val);
return root;
}
//二分法
TreeNode * parent = root,*curr = root;
while(curr!=nullptr){
parent = curr;
if(curr->val<val){
curr = curr->right;
}
else curr = curr->left;
}
if(parent->val<val){
parent->right = new TreeNode(val);
}
else parent->left = new TreeNode(val);
return root;
}
};
很暴力,很容易出现二叉搜索树退化为链表的情况。
定义红黑树的节点
好的算法先从数据结构开始。由于红黑树的每个节点都有颜色属性,所以需要加一个color,非红即黑,因而用bool值即可,另外旋转过程中需要频繁的访问兄弟、父亲及叔叔节点,所以另加两个节点,parent以及brother,父亲的兄弟就是叔叔。
/**
* Definition for a binary tree node.
* struct RBTNode {
* int val;
* bool red;//插入默认是红色
* RBTNode *parent,*brother;
* RBTNode *left;
* RBTNode *right;
* RBTNode() : val(0), left(nullptr), right(nullptr),parent(nullptr),brother(nullptr),red(true) {}
* RBTNode(int x) : val(x), left(nullptr), right(nullptr) ,parent(nullptr),brother(nullptr),red(true) {}
* RBTNode(int x, RBTNode *left, RBTNode *right) : val(x), left(left), right(right) ,parent(nullptr),brother(nullptr),red(true) {}
* };
*/
左旋(AVL树直接复制过来)
RBTNode* leftRotate(RBTNode* root) {
RBTNode* oldRoot = root;
RBTNode* newRoot = root->right;
RBTNode* parent = root->parent;
//1.newRoot 替换 oldRoot 位置
if (nullptr != parent ) {
if (oldRoot->parent->data > oldRoot->data) {
parent->left = newRoot;
}else {
parent->right = newRoot;
}
}
newRoot->parent = parent;
//2.重新组装 oldRoot (将 newRoot 的左子树 给 oldRoot 的右子树)
oldRoot->right = newRoot->left;
if (newRoot->left != nullptr) {
newRoot->left->parent = oldRoot;
}
//3. oldRoot 为 newRoot 的左子树
newRoot->left = oldRoot;
oldRoot->parent = newRoot;
return newRoot;
}
右旋(AVL树直接复制过来)
RBTNode* rightRotate(RBTNode* root) {
RBTNode* oldRoot = root;
RBTNode* newRoot = root->left;
RBTNode* parent = root->parent;
//1.newRoot 替换 oldRoot 位置
if (nullptr != parent ) {
if (oldRoot->parent->data > oldRoot->data) {
parent->left = newRoot;
}else {
parent->right = newRoot;
}
}
newRoot->parent = parent;
//2.重新组装 oldRoot (将 newRoot 的左子树 给 oldRoot 的右子树)
oldRoot->left = newRoot->right;
if (newRoot->right != nullptr) {
newRoot->right->parent = oldRoot;
}
//3. oldRoot 为 newRoot 的左子树
newRoot->right = oldRoot;
oldRoot->parent = newRoot;
return newRoot;
}
程序员嘛,能复用绝不自己动手。
变色
node->color = !node->color;
//或者,直接指定
node->color = true;
node->color = false;
//当然也可以提前定义一下:
#define RED true
#define BLACK false
好了,准备工作也就差不多了,开始正式迈入红黑书
红黑树的插入
红黑树的插入情形,大牛们已经帮我们分析好了,目前先不管其为什么这样做,先实现再说。
需要说明的是,下文中描述的新节点并不一定是新插入的节点,也有可能是递归向上调整过程中所选取操作的节点。
情形1
新结点(A)位于树根。
根据红黑书的性质,根节点是黑色的,只要重新上色即可
体现在代码中:
void updateTree(RBTNode* root,RBTNode* N){
if(N==root){
N->color = BLACK;//也不用判断是否为红色,无所谓
return;
}
...
}
情形2
新节点(D)的父节点和叔节点都是红色的,违背了性质5。
这种情况下,只需要对D的父节点、叔节点、祖父节点进行变色即可。
体现在代码中:
void updateTree(RBTNode* root,RBTNode* N){
...
if(N->parent->color==RED&&N->parent->brother->color==RED){
N->parent->color = BLACK;
N->parent->brother->color = BLACK;
N->parent->parent->color = BLACK;
}
...
}
情形3
D的父节点是红色的,叔节点是黑色的,并且局部呈现直线
直线的意思是D的父节点、祖父节点构成一条直线,在本例中都是各自的左子节点。
这种情况下:
- 一般先旋转D的祖父节点,直线朝左则向右旋转,直线朝右则向左旋转,在本例中,直线朝左,则向右旋转。
- 对原来的父节点和祖父节点进行变色。
变色和选择是独立的,因此也可以先变色再旋转
体现在代码中:
void updateTree(RBTNode* root,RBTNode* N){
...
if(N->parent->color==RED&&N->parent->brother->color==BLACK){
if(N->val<N->parent->val&&N->parent->val<N->parent->parent->val){
//情形3,直线向左
N->parent->color = BLACK;
N->parent->parent->color = RED;
rightRotate(N->parent->parent);
}
else if(N->val>N->parent->val&&N->parent->val>N->parent->parent->val){
//情形3,直线向右
N->parent->color = BLACK;
N->parent->parent->color = RED;
leftRotate(N->parent->parent);
}
}
...
}
情形4
D的父节点是红色的,叔节点是黑色的,并且局部呈现折线
折线的意思是D的父节点和祖父节点呈一条折线
对此,我们要旋转D的父节点,将其旋转为一条直线,变为情形3
代码上,情形3和4可以整合到一起。
void updateTree(RBTNode* root,RBTNode* N){
if(N==root){
//情形1
N->color = BLACK;//也不用判断是否为红色,无所谓
return;
}
else if(N->parent->color==BLACK){
return;
}
else if(N->parent->brother->color==RED){
//情形2
N->parent->color = BLACK;
N->parent->brother->color = BLACK;
N->parent->parent->color = BLACK;
}
else if(N->parent->brother->color==BLACK){
if(N->parent->val<N->parent->parent->val){
if(N->val>N->parent->val){
//情形4
N = N->parent;
leftRotate(N);
}
//情形3,直线向左
N->parent->color = BLACK;
N->parent->parent->color = RED;
rightRotate(N->parent->parent);
}
else if(N->parent->val>N->parent->parent->val){
if(N->val>N->parent->val){
//情形4
N = N->parent;
rightRotate(N->parent);
}
//情形3,直线向右
N->parent->color = BLACK;
N->parent->parent->color = RED;
leftRotate(N->parent->parent);
}
}
}
红黑书的删除节点操作
删除红色节点
从红黑树的性质中可以知道,删除红色节点不会破坏红黑树原有的性质。
原因有二:一,删除红节点不会影响红黑树的黑高;二,不会产生相邻两个红节点。
删除黑色节点
我们在删除黑色节点后,会破坏红黑树的性质,我们对删除节点后取代原节点位置的节点及其相关节点的颜色进行讨论,分四种情况修复红黑树的性质。
我们将被删除节点记为Z,删除节点后取代原节点位置的节点,记为X ,其兄弟节点记为W。
情形1
W是黑色,且其子节点都是黑色
也就是说X的叔节点和侄子节点都是黑色,注意节点A并不一定是红色,它的颜色暂不影响下面的操作。
如上图Z节点被删除后,左侧子树的黑高减小1,为了维护红黑性质,需要将右侧子树的黑高同样减小1,如此只需要将W变为红色即可。
但这样并未处理结束,因为这样操作仅仅是让这棵子树的黑高平衡,如果把这棵子树放到整体中看并不一定是平衡的,比如在整体中A是某个节点的左子树。我们只是将这个矛盾从局部上移了。
接下来另X的父节点成为X,继续进行删除修复工作,直到X为根节点或X的颜色为红色时退出,退出之后再将X变为黑色,保证不会出现相邻的红色节点即可。
就本例而言,W染为红色后,将A节点重新上色为黑色即可。
下面是代码实现:
void deleteFix(RBTNode *X){
if(X->parent==nullptr||X->color==RED){
X->color = BLACK;return;
}
if(X->brother->color==BLACK&&X->brother->left->color==BLACK&&X->brother->right->color==BLACK){
//情形1
X->brother->color = RED;
X = X->parent;
deleteFix(X);return;
}
...
}
情形2
W是黑色,且其右节点为红色
这种情况的处理分为以下三步:
- 将W的颜色设置为X父节点的颜色
- 将X父节点以及W右节点染黑。
- 左旋X父节点
下面是代码实现:
void deleteFix(RBTNode *X){
if(X->parent==nullptr||X->color==RED){
X->color = BLACK;return;
}
...
if(X->brother->color==BLACK&&X->brother->right->color==RED){
//情形2
X->brother->color = X->parent->color;
X->parent->color = BLACK;
X->brother->right->color = BLACK;
leftRotate(X->parent);return;
}
...
}
情形3
W是黑色,且其子节点左红右黑
- 先交换W和W左子节点的颜色,这样W就变为红色。
- 接下来对W进行右旋
如此变为情形2。
体现在代码中为:
void deleteFix(RBTNode *X){
if(X->parent==nullptr||X->color==RED){
X->color = BLACK;return;
}
...
if(X->brother->color==BLACK&&X->brother->left->color==RED&&X->brother->right->color==BLACK){
//情形3
swap(X->brother->color,X->brother->left->color);
X->brother = rightRotate(X->brother);
//接下来进行情形2操作
return;
}
...
}
情形4
W是红色
- 将W与X父节点颜色对调
- 然后对X的父节点进行左旋操作。
如此一来,矩形框所框部分则又到了情形1到情形3,递归处理即可
完整代码如下:
void deleteFix(RBTNode *X){
if(X->parent==nullptr||X->color==RED){
X->color = BLACK;return;
}
if(X->brother->color==BLACK&&X->brother->left->color==BLACK&&X->brother->right->color==BLACK){
//情形1
X->brother->color = RED;
X = X->parent;
deleteFix(X);return;
}
if(X->brother->color==BLACK&&X->brother->right->color==RED){
//情形2
X->brother->color = X->parent->color;
X->parent->color = BLACK;
X->brother->right->color = BLACK;
leftRotate(X->parent);return;
}
if(X->brother->color==BLACK&&X->brother->left->color==RED&&X->brother->right->color==BLACK){
//情形3
swap(X->brother->color,X->brother->left->color);
X->brother = rightRotate(X->brother);
//接下来进行情形2操作
deleteFix(X);
return;
}
if(X->brother->color==RED){
swap(X->brother->color,X->parent->color);
leftRotate(X->parent);
deleteFix(X);
return;
}
}
感想
路虽远,行则将至。