手写红黑树
前言
所有的图片挂载于github,如不能查看,请检查网络信息。
文章主要进行红黑树的代码实现与原理阐释,对于基础的数据结构概念,本文不再做详细解释。
由于时间原因,实现原理与实现方式会直接以代码+注释的形式给出
什么是红黑树
在所有的数据结构中,存储类数据结构所面临的问题一直有两个:排序和查找。
前者表示了数据结构的设计是否是有序的,对于很多业务需求来说,排序是刚需,如何保证数据不断地增减、修改的情况下,仍保持有序,是对数据结构的基本需求。后者则是出于性能上的考虑,当数据量足够大的时候,如果查找时间虽数据量线性增长,那显然也是不可接受的。
基于这两点,我们提出非一维的数据结构,例如树状结构、图状结构。并且,提供了一套完整的方案来进行数据的处理。
鉴于代码实现的简单性与查找的便利性(一次查询需要排除尽可能多的非法数据),我们提出一种二叉树结构,在此基础上,又提出了平衡树、搜索树结构。
而红黑树,就是一颗近似二叉搜索平衡树。他可以以O(logn)的时间复杂度,完成数据的增删改查。
在实现红黑树之前,我们先来复习一下红黑树的数据结构约定:
1.所有节点都是红色或者黑色节点
2.根节点一定为黑色节点
3.叶子结点也一定为黑色节点(可以为NIL)
4.从任意一个节点出发,其所有的完整路径黑高相等
5.红色节点不能相邻(不能有父子关系)
现在,我们正式开始实现红黑树
构建红黑树节点
首先,是构建节点,我们需要定义节点的基础数据结构,包含颜色、数据内容、父子指针的定义,完整内容如下:
#define RED 0
#define BLACK 1
#define NOCOLOR 2
enum color
{
red = RED,
black = BLACK,
noclor=NOCOLOR
};
struct rb_node
{
color color = red;
TVal val;
rb_node* parent = nullptr;
rb_node* left = nullptr;
rb_node* right=nullptr;
};
然后,我们需要构造析构函数,包括树的构造析构与节点的构造析构。
rbtree()
{
int a = 0;
rb_node *newnode = buyNode(a);
NIL = newnode;
NIL->color = black;
_root_node = NIL;
}
virtual ~rbtree()
{
destroy(_root_node);
delete Nil; Nil = nullptr;
}
void destroy(rb_node*& root)
{
if (root == NIL)
{
return;
}
if (root->left != NIL)
{
destroy(root->left);
}
if (root->right != NIL)
{
destroy(root->right);
}
delete root;
root = NULL;
}
节点本身的构造析构使用缺省的就可以,无需浪费时间,但是我们需要再红黑树这个类里面进行节点的构造,所以单独写一个函数。具体内容如下:
rb_node* buyNode()
{
int val = 0;
return buyNode(val);
}
rb_node* buyNode(const TVal& ival = TVal())
{
using pNode = rbtree<TVal>::rb_node;
pNode *pnode = new rb_node();
pnode->val = ival;
if (NIL)
{
pnode->parent = NIL;
pnode->left = NIL;
pnode->right = NIL;
}
return std::move(pnode);
}
左旋与右旋,因为左旋右旋比较简单,仅注释了容易出错的一步:
//左旋
void leftRotate(rb_node*& t)
{
if (!t)return;
rb_node* tp = t->parent;
rb_node* tr = t->right;
if (tr == NIL)return;//根本没有左旋的条件
t->right = tr->left;//这一步提前,因为后续会修改tr->left
if (t == _root_node)//第二步是修改两个节点的父
{
_root_node = tr;
}
else
{
if (tp->left == t)//target node is left child
{
tp->left = tr; tr->parent = tp;//修改父节点
}
else if(tp->right == t)
{
tp->right = tr; tr->parent = tp;//修改父节点的关系
}
}
t->parent = tr;//最后再修改两个节点之间的之间关系
tr->left = t;
}
//右旋(处理当前节点与节点的左孩子)
void rightRotate(rb_node* &t)//input target
{
if (!t)return;
rb_node* tp = t->parent;
rb_node* tl = t->left;
if (tl == NIL)return;
t->left = tl->right;
if (t==_root_node)
{
_root_node = tl;
}
else
{
if (tp->left==t)
{
tp->left = tl;
}
else if (tp->right==t)
{
tp->right = tl;
}
}
t->parent = tl;
tl->right = t;
}
所有对于红黑树的修改都要优先置于叶子或者近似叶子节点来操作。插入的情况大致可以分为两种,即仅仅需要染色和需要调整旋转两种方式:
//插入
void insertFix(rb_node*& t)
{
rb_node* tp = t->parent;
while (t->parent->color==red)//因为插入节点是红的,不影响黑高;如果上级为黑,什么都不用干
{
//以下总共六个分支,需要记忆一共两种情况(直接染色+调整旋转)
rb_node* tpp = tp->parent;//此时tpp节点应该是黑的
if (tp== tpp->left)//当前tp是tpp左孩子
{
rb_node* tppr = tpp->right;//uncle节点
if (tppr->color==red)//叔父节点与父节点同为红色
{
tppr->color = black;
tp->color = black;
tpp->color = red;
t = tpp;
}
else
{
if (t == tp->right)//当前节点为l-r,左旋调整一下
{
leftRotate(tp);
}
if(t == tp->left)//当前节点是左孩子[达成l-l,准备右旋]
{
tp->color = black;
tpp->color = red;
rightRotate(tpp);
}
}
}
else if (tp == tpp->right)
{
rb_node* tppl = tpp->left;//uncle节点
if (tppl->color == red)//叔父节点与父节点同为红色
{
tppl->color = black;
tp->color = black;
tpp->color = red;
t = tpp;
}
else
{
if (t == tp->left)//当前节点为r-l,右旋调整一下
{
rightRotate(tp);
}
if (t == tp->right)//当前节点是右孩子[达成r-r,准备左旋]
{
tp->color = black;
tpp->color = red;
leftRotate(tpp);
}
}
}
}
_root_node->color = black;
}
void insertNode(TVal &ival)
{
rb_node* newnode = buyNode(ival);
insertNode(newnode);
}
void insertNode(rb_node*& t)
{
if (_root_node==NIL)
{
_root_node = t; _root_node->color = black; return;
}
rb_node* frontpos = _root_node;//查询插入的位置(插入节点的父节点)(一定为叶子节点)
rb_node* temp = _root_node;//temp的边界条件为nil
while (temp != NIL)
{
frontpos = temp;
if (t->val < temp->val)
temp = temp->left;
else
temp = temp->right;
}
t->parent = frontpos;
//我们需要决定把它插在左还是右上面
if (t->val>frontpos->val)
{
frontpos->right = t;
}
else
{
frontpos->left = t;
}
//调整插入之后的红黑树(从插入的那个节点开始)
insertFix(t);
}
删除的重心在于对黑高的把握,注意在下文中的红黑树旋转的注解
rb_node* successor(rb_node* t)
{
if (t->right != NIL)
{
rb_node* afterNode = _root_node;//这是一个叶子节点(或者单叶子节点)
rb_node* temp = t->right;
while (temp != NIL)
{
afterNode = temp;
temp = temp->left;
}
return afterNode;
}
//要是没有右子,那么后继节点就只能寄希望于我们当前在一颗左树上
//这棵树的父节点就是我们要的后继节点
rb_node* tp = t->parent;
while (tp != NIL && t == tp->right)
{
t = tp;
tp = t->parent;
}
return tp;
}
void removeFix(rb_node* t)
{
if (t->color==red)//如果当前节点是红色的,那刚好可以弥补之前的删除节点
{
t->color = black;
return;
}
while (t!=_root_node && t->color==black)//边界条件:从下向上,处理黑高
{
//前提条件是t所在的分支黑高被减一,已经不平衡了
rb_node* tp = t->parent;//注意,此时的tp可以是红色或者是黑色,但是我们不分开讨论(剪枝)
if(tp->left==t)
{
rb_node* tpr = tp->right;
if (tpr->color==red)//这里意味着tpp当前是黑色的,父节点是黑色的不利于我们进行两颗子树的黑高调整
{
//当前tpr是红的,那么tprl-tprr都是黑色
tpr->color = black;
tp->color = red;
leftRotate(tp);//转完之后,tprl就成为了tp的右节点
//指向刷新
tp= t->parent;//此时tp有了黑色的右节点
tpr = tp->right;//tpr为黑色
}
//此时,虽然有t的路径上,黑高还是减一的状态,但是tpr已经是黑色的了
if (tpr->left->color==black && tpr->right->color==black)
{
//如果在t的叔父上也把黑高减一,两边将重新平衡
//因为现在已经有了一个黑三角(tpr|tprl|tprr),把父节点染红就可以了
tpr->color = red;
t = tp;//此刻的tp应该是红色的
}
else if (tpr->right->color == black)
{
//此时无法再在t的叔父上也把黑高整体减一,但是我们可以减去一半
//将tpr左边的黑高减一
tpr->color = red;
tpr->left->color = black;
rightRotate(tpr);//此时tpr已经不是tp的右子节点了,但是后面我们不用他了,也就不在刷新
t = tp->right;//此时新的不平衡问题转移到了减到一半的tpr上面
}
else
{
//此时tpr可能有两个红色节点,且右节点一定是红的(rr难以组成)
tpr->color = tp->color;//tpr的路径黑高减一(准备拆分tprr|tprl)
tp->color = black;//整体黑高加一(为左转准备)
tpr->right->color = black;//tprr黑高加一(它不跟着左转,所以单独加一了)
leftRotate(tp);//tprr将会留在右边,而tprl将作为t的兄弟出现(他两黑高目前一致)
//全部整完,黑高平衡了
t = _root_node;
}
}
else
{
rb_node* tpl = tp->left;
if (tpl->color == red)//这里意味着tpp当前是黑色的,父节点是黑色的不利于我们进行两颗子树的黑高调整
{
//当前tpl是红的,那么tpll-tplr都是黑色
tpl->color = black;
tp->color = red;
leftRotate(tp);//转完之后,tpll就成为了tp的右节点
//指向刷新
tp = t->parent;//此时tp有了黑色的右节点
tpl = tp->right;//tpl为黑色
}
//此时,虽然有t的路径上,黑高还是减一的状态,但是tpl已经是黑色的了
if (tpl->left->color == black && tpl->right->color == black)
{
//如果在t的叔父上也把黑高减一,两边将重新平衡
//因为现在已经有了一个黑三角(tpl|tpll|tplr),把父节点染红就可以了
tpl->color = red;
t = tp;//此刻的tp应该是红色的
}
else if (tpl->right->color == black)
{
//此时无法再在t的叔父上也把黑高整体减一,但是我们可以减去一半
//将tpl左边的黑高减一
tpl->color = red;
tpl->left->color = black;
rightRotate(tpl);//此时tpl已经不是tp的右子节点了,但是后面我们不用他了,也就不在刷新
t = tp->right;//此时新的不平衡问题转移到了减到一半的tpl上面
}
else
{
//此时tpl可能有两个红色节点,且右节点一定是红的(rr难以组成)
tpl->color = tp->color;//tpl的路径黑高减一(准备拆分tplr|tpll)
tp->color = black;//整体黑高加一(为左转准备)
tpl->right->color = black;//tplr黑高加一(它不跟着左转,所以单独加一了)
leftRotate(tp);//tplr将会留在右边,而tpll将作为t的兄弟出现(他两黑高目前一致)
//全部整完,黑高平衡了
t = _root_node;
}
}
}
}
void removeNode(TVal& ival)
{
rb_node* pNode = searchNode(ival);
if (pNode)
{
removeNode(pNode);
}
}
void removeNode(rb_node*& t)
{
//先找后继节点(删除和插入一样,需要从尽量叶子节点出发,简单一些)
rb_node* pdeleteNode = nullptr;
if (t->left==NIL || t->right==NIL)//t本身是叶子节点(或者是单孩子节点)
{
pdeleteNode = t;
}
else
{
pdeleteNode=successor(t);//此时后继节点是在t->right上面找的,所以返回的节点也是单孩子或者叶子
}
//现在删除的节点一定为叶子或者单孩子节点
rb_node* pdeleteChild = (t->left == NIL) ? t->right : t->left;
//把删除节点的子节点顶上来
if (pdeleteNode == _root_node)
_root_node = pdeleteNode;
else if (pdeleteNode == pdeleteNode->parent->left)
pdeleteNode->parent->left = pdeleteChild;
else
pdeleteNode->parent->right = pdeleteChild;
pdeleteChild->parent = pdeleteNode->parent;
//把要删除的节点的val给我们查找的那个节点(一个是要删除值,一个是要删除物理地址)
t->val = pdeleteNode->val;
//检测当前的行为是否会影响黑高(到目前位置,只进行了一次删除pdeleteNode)
if (pdeleteNode->color==black)
{
//由于删除了一个黑色节点,我们可能会导致黑高发生变化
//或者会导致两个红色节点相邻
removeFix(pdeleteChild);
}
}
Q.E.D.