目录
红黑树的概念
红黑树的性质
红黑树的插入
红黑树的查找
红黑树的验证
红黑树的删除操
为什么有了AVL树还需要红黑树?
AVL树的出现解决了二叉树会退化为单链表的情况,能把搜索时间控制在O(logN),不过却不是最佳的。因为AVL树的插入和删除,可能会破坏AVL树中的左子树和右子树高度差不超过1的规则,即大量的插入结点和删除结点可能会引发频繁的旋转操作。使得AVL树的性能大打折扣,为了解决AVL树会频繁旋转的问题,又发明了红黑树。
红黑树是二叉搜索树中的一种,是在搜索二叉树的基础上在每个结点中增加了1个存储位,用于表示结点的颜色,颜色可以是红色,也可以是黑色。因此称红黑树。红黑树是通过对任何一条从根结点到叶子结点的路径上的各个结点的颜色进行限制,确保最长路径的结点数不超过最短路径的结点数的两倍,所以红黑树还是近似平衡的。
1.每个结点不是红色就是黑色。
2. 红黑树的根结点是黑色的。
3.如果一个结点是红色的,那么它的孩子结点都是黑色的,即一条路径上不能同时出现两个连续的红色结点。4.对于每个结点,从该结点到其所有后代的叶子结点的简单路径上,均包含相同数目的黑色结点。
5、每个叶子结点都是黑色的(指空结点)。
为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?
从红黑树的性质3可以得出,红黑树中不会出现连续的红色结点,而根据性质4,可以从任何一个结点其后代叶子结点的所有路径土包含的黑色结点数相同。
假设有一个结点其到叶子结点的最短路径上的结点全部为黑色结点设黑色结点数为N,而最长路径可能是由一黑一红构成的路径,其路径上的黑色结点树也为N,红色结点也N总结点数为2N,因此最长路径不会超过最短路径的两位倍。
思考:在向红黑树插入新增结点时,默认应该没为红色还是黑色?
在未插入新增结点前,红黑树中,任何一个结点到叶子结点的路径上的黑色结点数目都是一样的。
若新增结点默认为黑色,就一定破坏这个规则,即违反红黑树的性质4,此时就需要对红黑树进行调整。
若新增结点默认为红色,如果此时父结点也是红色的,那么就出现了连续的红色结点,即违反了红黑树的性质3,此时也需要对红黑树进行调整。但如果新增结点的父结点是黑色的就不需要进行调整,也就是说如果新结点默认为黑色就,一定会破坏性质4,必须进行调整处理。新增结点默认为红色,可能会违反性质3,可能要进行调整处理。
即默认黑色一定要进行调整,默认红色不一定要调整,因此在构选新增结点时,我们将默颜色设为红色。
结点构造
enum clour
{
RED,
BLACK
};
template
struct RBTreeNode
{
//三叉链
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
pair _kv;
//存储的键对值
//结点的颜色
int _col;//红or黑
//构造函数
RBTreeNode(const pair& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv),_col(RED)//默认红色
{}
};
其步骤为三步:
1.按照新增结点中的key值与前结点的ken值相比,小于则往该结点的左子树走,大于则往该结点右边走,等于则插入失则,走到空就是找到了插入的置位。
2.进行插入操作。
3.插入后进行判断是否要进行调整操作。
红黑树的调整操作
当插入新增结点后,如果新增结的父结点颜色为黑色则不需要调整操作,因为此时并不违反红黑树性质。
如果插入新增结的父结点颜色为红色则需要进行调整操作,即出现了连续红色结点违反了规则3。 ,
当新增结点的父结点颜色是红色时说明其父结点不是树的根结点,红黑树的根结点是黑色的。因此说明新增经点的祖父结点一定存在,红黑树的调整操作是根据新增结点的叔叔结点(父结点的兄弟结点)的颜色和是否存在的情况进行相应的调整操作的。
叔叔结点的存在与否和颜色情况可以分为三种:
1.叔叔结点存在且颜色为红色。
2.叔叔结点存在且颜色为黑色。
3.叔叔结点不存在。
情况一:叔对结点存在且颜色为红色。
当叔叔存在为红色时,将新增结点的父结点的颜色变为黑色,将祖父结点变红色,再将叔叔结点的颜色变为黑色,这样即保持了每条是各径上的黑色结点数目都保持不变,也解决的出现连续红色结点的问题。
但调整并没有结束,因为祖父结点的颜色为红色,如果组父结点为整棵树的根结点时,祖父结点变为红色,就违反了红黑树根为黑的性质。此时我们直接将祖文结点的颜色改为黑色即可,相当于每条路径都增加了一个黑色结点。
但如果祖父结点不是红黑树的根结点时,我们就要将不结点传为新增结点,再判断祖父结点的父结点颜色是否为红色,若为红色,则又根据祖父结点的叔叔结点颜色及存在情况再进行调整操作。所以叔叔结点存在且颜色为红色的调整操作如图:
注意,新增结点的叔叔结点存在时,不管新增结点是左孩子结点,还是右孩子结点其操作都是一样的。
情况二:叔叔结点存在且颜色为黑色。
这种情况是因为情况一向上调整出现的,即这种情况下的cur结点不是新增结点,我们可以通过下图分析。
我们将红黑树根结点到图中的祖父结点之间的黑色结点数目设为x(不包含祖父结点),将叔叔结点之下的黑色结点数目设为y,所以两条路径上的黑色结点数盼别为x+1、x+2+y,很显然x+1一定不等于x+2+y,不满足红黑树的要求,即不存在新增结点的叔叔存在且颜色为黑色的情况,这种情况是情况一在调整过程中出现的。
注意:这里说的路径指的是从根结点到指针指向空的位置,而不是只有从根结点到叶子结点才能算路径。
当出现情况二时,使用单纯的变色已无法处理,这时就要通过先旋转再变色完调整。
若cur结点位于父结点的左子树,父结点位于祖父结点的左子树,则进行右单旋,旋转完后将父结点颜色变为黑色,将祖父结点的颜色变为红色,这时就不再需要继续向上调整了。
若cur结点位于父结点的右子树,父结点位于祖父结点的右子树,则进行左单旋,旋转完后将父结点颜色变为黑色,将祖父结点的颜色变为红色,这时就不再需要继续向上调整了。
若cur结点位于父结点的右子树,父结点位于祖父结点的左子树,则进行左右双旋,旋转完后将父结点颜色变为黑色,将祖父结点的颜色变为红色,这时就不再需要继续向上调整了。
若cur结点位于父结点的左子树,父结点位于祖父结点的右子树,则进行右左双旋,旋转完后将父结点颜色变为黑色,将祖父结点的颜色变为红色,这时就不再需要继续向上调整了。
旋转完后将新的根结点颜色变为黑色,这时就不再需要继续向上调整了。
情况三:新增结点的叔叔结点不存在。
这种情况不可能是由情况一调整而来的,因为叔叔结点不存在说明parent下面没有黑色结点了,不然就违返了路径的黑色结点数目相同原则,所以cur一定是新增结点。
若cur结点位于父结点的左子树,父结点位于祖父结点的左子树,则进行右单旋,旋转完后将父结点颜色变为黑色,将祖父结点的颜色变为红色,这时就不再需要继续向上调整了。
右单旋代码
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* pparent = parent->_parent;
//1、将subRL链接称为parent的右子树。
parent->_left = subLR;
if (subLR)
{
subRL->_parent = parent;
}
//2、让parent成为subR的左子树。
subL->_right = parent;
parent->_parent = subL;
//3、让subR成为这颗最小不平衡子树的根。
if (pparent==nullptr)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
if (pparent->_left == parent)
{
pparent->_left = subL;
subL->_parent = pparent;
}
if (pparent->_right == pparent)
{
pparent->_right = subL;
subL->_parent = pparent;
}
}
}
若cur结点位于父结点的右子树,父结点位于祖父结点的右子树,则进行左单旋,旋转完后将父结点颜色变为黑色,将祖父结点的颜色变为红色,这时就不再需要继续向上调整了。
左单旋代码
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* pparent = parent->_parent;
// 1、将subRL链接称为parent的右子树。
parent->_right = subRL;
if (subRL)
{
subRL->_parent = parent;
}
//2、让parent成为subR的左子树。
subR->_left = parent;
parent->_parent = subR;
//3、让subR成为这颗最小不平衡子树的根。
if (pparent == nullptr)//parent为根结点
{
_root = subR;//更新subR为根结点
_root->_parent = nullptr;
}
else//parent不为根结点
{
if (pparent->_left == parent)
{
subR->_parent==pparent;
pparent->_left = subR;
}
else
{
subR->_parent == pparent;
pparent->_right = subR;
}
}
}
若cur结点位于父结点的右子树,父结点位于祖父结点的左子树,则进行左右双旋,旋转完后将父结点颜色变为黑色,将祖父结点的颜色变为红色,这时就不再需要继续向上调整了。
左右双旋代码
void RotateLR(Node* parent)
{
RotateL(parent->_left);
RotateR(parent);
}
若cur结点位于父结点的左子树,父结点位于祖父结点的右子树,则进行右左双旋,旋转完后将父结点颜色变为黑色,将祖父结点的颜色变为红色,这时就不再需要继续向上调整了。
//右左双旋
void RotateRL(Node* parent)
{
RotateR(parent->_right);
RotateL(parent);
}
插入函数代码:
//插入函数
pair Insert(const pair& kv)
{
if (_root == nullptr) //若红黑树为空树,则插入结点直接作为根结点
{
_root = new Node(kv);
_root->_col = BLACK; //树的根结点的颜色必须是黑色
return make_pair(_root, true);
}
//1、按照key值小于当前结点的key值往左走
//key值大于当前结点的key值往右走,找到待插入的位置
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (kv.first < cur->_kv.first) //待插入结点的key值小于当前结点的key值
{
//往该结点的左子树走
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first) //待插入结点的key值大于当前结点的key值
{
//往该结点的右子树走
parent = cur;
cur = cur->_right;
}
else //key值冗余插入失败
{
return make_pair(cur, false);
}
}
//找到了
//2、新增结点插入到树中
cur = new Node(kv); //构造新结点
Node* newnode = cur;
if (kv.first < parent->_kv.first) //新增结点的key值小于parent的key值
{
//新增结点插入到parent的左边
parent->_left = cur;
cur->_parent = parent;
}
else //新增结点的key值大于parent的key值
{
//新增结点插入到parent的右边
parent->_right = cur;
cur->_parent = parent;
}
//3、若插入结点的父结点是红色的,对红黑树进行调整
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent; //parent的颜色是红色,则parent不是根结点
if (parent == grandfather->_left) //parent是grandfather的左孩子
{
Node* uncle = grandfather->_right; //uncle是grandfather的右孩子
if (uncle && uncle->_col == RED) //情况1:uncle存在且为红
{
//调整颜色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//继续往上调整
cur = grandfather;
parent = cur->_parent;
}
else //uncle不存在和uncle存在且为黑
{
if (cur == parent->_left)
{
//先进行右单旋再调整颜色
RotateR(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
else
{
//先进行左右双旋再调整颜色
RotateLR(grandfather);
grandfather->_col = RED;
cur->_col = BLACK;
}
break; //子树旋转后,该子树的根变成了黑色,无需继续往上进行处理
}
}
else //parent是grandfather的右孩子
{
Node* uncle = grandfather->_left; //uncle是grandfather的左孩子
if (uncle && uncle->_col == RED) //uncle存在且为红
{
//调整颜色
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
//继续往上调整
cur = grandfather;
parent = cur->_parent;
}
else //uncle不存在和uncle存在且为黑
{
if (cur == parent->_left)
{
//先进行右左双旋再调整颜色
RotateRL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
else
{
//先进行左单旋再调整颜色
RotateL(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
break; //该子树的根变成了黑色,无需继续往上进行处理
}
}
}
_root->_col = BLACK; //根结点的颜色为黑色(可能被情况一变成了红色,需要变回黑色)
return make_pair(newnode, true); //插入成功
}
红黑树的查找函数与二叉搜索树的查找方式一模一样,逻辑如下:
若树为空树,则查找失败,返回nullptr。
若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
若key值等于当前结点的值,则查找成功,返回对应结点。
查找函数代码
Node* Find(const K& key)
{
//从根结点开始,若树为空树,
Node* cur = _root;
while (cur)
{
if (key < cur->_kv.first) //若key值小于当前结点的值,则往当前结点的左子树当中进行查找。
{
cur = cur->_left; //在该结点的左子树当中查找
}
else if (key > cur->_kv.first) //若key值大于当前结点的值,则往当前结点的右子树当中进行查找。
{
cur = cur->_right; //在该结点的右子树当中查找
}
else //若key值等于当前结点的值,则查找成功,返回对应结点。
{
return cur; //返回该结点
}
}
return nullptr; //树中没有key值查找失败,返回nullptr。
}
bool ISRBTree()
{
if (_root == nullptr) //空树是红黑树
{
return true;
}
if (_root->_col == RED)
{
cout << "error:根结点为红色" << endl;
return false;
}
Node* cur = _root;
int BlackCount = 0;
while (cur)
{
if (cur->_col == BLACK)
BlackCount++;
cur = cur->_left;
}
int count = 0;
return _ISRBTree(_root, count, BlackCount);
}
bool _ISRBTree(Node* root, int count, int BlackCount)
{
if (root == nullptr)
{
if (count != BlackCount)
{
cout << "error:黑色结点的数目不相等" << endl;
return false;
}
return true;
}
if (root->_col == RED&&root->_parent->_col == RED)
{
cout << "error:存在连续的红色结点" << endl;
return false;
}
if (root->_col == BLACK)
{
count++;
}
return _ISRBTree(root->_left, count, BlackCount) && _ISRBTree(root->_right, count, BlackCount);
}
查找并进行删除操。
查找的逻辑与查找函数一致,找到待删除结点后,若待删除结点的左右子树均不为空,需要先找到“替代结点N”来替代待删除结点而被实际的删除,也就是说删除的是替代点,而替代结点至少有一个子结点为空。
若替代结点N为红色,则两个子结点一定都空,直接将替代结点删除,不违反红黑树任何性质,此时删除操作结束。
若替代结点为黑色, 替代结点N的一个子结点不为空,则这个子结点一定是红色的,且这个子结点的子结点都是空(黑高性质),那么将这个子结点占有替代结点的位置,并且这个结点的颜色变为黑色,删除替代结点。不违反红黑树性质,删除操作结束。
若替代结点N为黑色,另一个子结点也为空,把替代结点N删除后,违反黑高,那么此时就要以替代结点为起点进行情况分析,然后执行相应操作。这里将替代结点叫N结点,P为父结点 S为N的兄弟结点。
情况一:S为红色(那么P为黑色,子节点也黑)。
操作以P为根结点将子树进行左单旋,然后变P红,S变黑。
在这种情况下:P左边少了一个黑色结点,旋转后N头上增加一个红色结点,但通过N的路径仍少一个黑结点需要再对N的情况进一步分析。
情形2:P、S及S的孩子们都为黑。
操作:S改为红色,未结束。
解析:S变为红色后经过S节点的路径的黑节点数目也减少了1,那个从P出发到其叶子节点到所有路径所包含的黑节点数目(记为num)相等了。但是这个num比之前少了1,因为左右子树中的黑节点数目都减少了!一般地,P是他父节点G的一个孩子,那么由G到其叶子节点的黑节点数目就不相等了,所以说没有结束,需把P当做新的起始点开始向上检索。
情形3:P为红(S一定为黑),S的孩子们都为黑。
操作:P该为黑,S改为红,结束。
解析:这种情况最简单了,既然N这边少了一个黑节点,那么S这边就拿出了一个黑节点来共享一下,这样一来,S这边没少一个黑节点,而N这边便多了一个黑节点,这样就恢复了平衡,多么美好的事情哈!
情形4:P任意色,S为黑,N是P的左孩子,S的右孩子SR为红,S的左孩子任意(或者是N是P的右孩子,S的左孩子为红,S的右孩子任意)。
操作:SR(SL)改为黑,P改为黑,S改为P的颜色,P、S变换--这里相对应于AVL中的右右中的旋转(或者是AVL中的左左旋转),结束。
解析:P、S旋转有变色,等于给N这边加了一个黑节点,P位置(是位置而不是P)的颜色不变,S这边少了一个黑节点;SR有红变黑,S这边又增加了一个黑节点;这样一来又恢复了平衡,结束。
情形5:P任意色,S为黑,N是P的左孩子,S的左孩子SL为红,S的右孩子SR为黑(或者N是P的有孩子,S的右孩子为红,S的左孩子为黑)。
操作:SL(或SR)改为黑,S改为红,SL(SR)、S变换;此时就回到了情形4,SL(SR)变成了黑S,S变成了红SR(SL),做情形4的操作即可,这两次变换,其实就是对应AVL的右左的两次旋转(或者是AVL的左右的两次旋转)。
解析:这种情况如果你按情形4的操作的话,由于SR本来就是黑色,你无法弥补由于P、S的变换(旋转)给S这边造成的损失!所以我没先对S、SL进行变换之后就变为情形4的情况了,何乐而不为呢?
以下代码参考:2021dragon
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
Node* delParentPos = nullptr;
Node* delPos = nullptr;
while (cur)
{
if (key < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (key > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else
{
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = _root->_right;
if (_root)
{
_root->_parent = nullptr;
_root->_col = BLACK;
}
delete cur;
return true;
}
else
{
delParentPos = parent;
delPos = cur;
}
break;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = _root->_left;
if (_root)
{
_root->_parent = nullptr;
_root->_col = BLACK; //根结点为黑色
}
delete cur; //删除原根结点
return true;
}
else
{
delParentPos = parent; //标记实际删除结点的父结点
delPos = cur; //标记实际删除的结点
}
break; //进行红黑树的调整以及结点的实际删除
}
else //待删除结点的左右子树均不为空
{
//替换法删除
//寻找待删除结点右子树当中key值最小的结点作为实际删除结点
Node* minParent = cur;
Node* minRight = cur->_right;
while (minRight->_left)
{
minParent = minRight;
minRight = minRight->_left;
}
cur->_kv.first = minRight->_kv.first; //将待删除结点的key改为minRight的key
cur->_kv.second = minRight->_kv.second; //将待删除结点的value改为minRight的value
delParentPos = minParent; //标记实际删除结点的父结点
delPos = minRight; //标记实际删除的结点
break; //进行红黑树的调整以及结点的实际删除
}
}
}
if (delPos == nullptr) //delPos没有被修改过,说明没有找到待删除结点
{
return false;
}
Node* del = delPos;
Node* delP = delParentPos;
if (delPos->_col == BLACK)
{
if (delPos->_left)
{
delPos->_left->_col = BLACK;
}
else if (delPos->_right)
{
delPos->_right->_col = BLACK;
}
else
{
while (delPos != _root)
{
if (delPos == delParentPos->_left)
{
Node* brother = delParentPos->_right;
if (brother->_col == RED)
{
delParentPos->_col = RED;
brother->_col = BLACK;
RotateL(delParentPos);
brother = delParentPos->_right;
}
if (((brother->_left == nullptr) || (brother->_left->_col == BLACK))
&& ((brother->_right == nullptr) || (brother->_right->_col == BLACK)))
{
brother->_col = RED;
if (delParentPos->_col == RED)
{
delParentPos->_col = BLACK;
break;
}
delPos = delParentPos;
delParentPos = delPos->_parent;
}
else
{
if ((brother->_right == nullptr) || (brother->_right->_col == BLACK))
{
brother->_left->_col = BLACK;
brother->_col = RED;
RotateR(brother);
brother = delParentPos->_right;
}
brother->_col = delParentPos->_col;
delParentPos->_col = BLACK;
brother->_right->_col = BLACK;
RotateL(delParentPos);
break;
}
}
else
{
Node* brother = delParentPos->_left;
if (brother->_col == RED)
{
delParentPos->_col = RED;
brother->_col = BLACK;
RotateR(delParentPos);
brother = delParentPos->_left;
}
if (((brother->_left == nullptr) || (brother->_left->_col == BLACK))
&& ((brother->_right == nullptr) || (brother->_right->_col == BLACK)))
{
brother->_col = RED;
if (delParentPos->_col == RED)
{
delParentPos->_col = BLACK;
break;
}
delPos = delParentPos;
delParentPos = delPos->_parent;
}
else
{
//情况三:brother为黑色,且其右孩子是红色结点,左孩子是黑色结点或为空
if ((brother->_left == nullptr) || (brother->_left->_col == BLACK))
{
brother->_right->_col = BLACK;
brother->_col = RED;
RotateL(brother);
//需要继续处理
brother = delParentPos->_left; //更新brother(否则执行下面情况四的代码会出错)
}
//情况四:brother为黑色,且其左孩子是红色结点
brother->_col = delParentPos->_col;
delParentPos->_col = BLACK;
brother->_left->_col = BLACK;
RotateR(delParentPos);
break; //情况四执行完毕后调整一定结束
}
}
}
}
}
//进行实际删除
if (del->_left == nullptr) //实际删除结点的左子树为空
{
if (del == delP->_left) //实际删除结点是其父结点的左孩子
{
delP->_left = del->_right;
if (del->_right)
del->_right->_parent = delP;
}
else
{
delP->_right = del->_right;
if (del->_right)
del->_right->_parent = delP;
}
}
else
{
if (del == delP->_left)
{
delP->_left = del->_left;
if (del->_left)
del->_left->_parent = delP;
}
else
{
delP->_right = del->_left;
if (del->_left)
del->_left->_parent = delP;
}
}
delete del;
return true;
}