手撕红黑树(Red-Black Tree)
Posted 林慢慢脑瓜子嗡嗡的
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手撕红黑树(Red-Black Tree)相关的知识,希望对你有一定的参考价值。
文章目录
⏰1.相关概念
🎄红黑树的定义
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是红色或者黑色。 最长路径的长度不超过最短路径的长度的2倍的二叉搜索树,因而红黑树是近似平衡的。在红黑树中,为了便于规范,将空结点(NIL)认为是叶子节点,保证叶子结点一定是黑色的。
🎄红黑树的性质
1.每个结点不是红色就是黑色
2.根节点、叶子结点(此处的叶子结点指的是空结点)是黑色的
3.如果一个节点是红色的,则它的两个孩子结点是黑色的
4.对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
由性质三知道红黑树中不存在连续的两个红色结点,由此推出在一条路径上红色结点的数量一定不会超过黑色结点的数量;而由性质四知道红黑树的每条路径上都有着相同数量的黑色结点。
以三个黑色结点为例
最短路径:
最长路径:
⏰ 进一步的,我们可以得出结论(如上图):红黑树的最短路径为全是黑色结点,最长路径为全是红黑节点各占一半。如此即可保证红黑树的最长路径不超过最短路径的两倍。那么红黑树增删查改的效率就在logN-2logN之间,和AVL树是一个量级。
AVL树是通过高度来控制平衡的,是严格平衡的。那如果新插入结点很多那么旋转也是要付出代价的。红黑树通过颜色来控制平衡,但不是严格的平衡,它近似平衡。红黑树也可以达到AVL树的效率。它最长路径不超过最短路径的2倍。
🎄红黑树与AVL树的比较
共同点:红黑树与AVL树都是平衡的二叉树
- AVL树是严格平衡的,而红黑树是近似平衡的
- AVL树和红黑树的查找时间复杂度都是O(log2N)
- 由于红黑树旋转次数更少,因此在增删过程中性能较优
⏰2.红黑树的实现
📕红黑树的结点定义
与AVL树并无太大的不同,红黑树的结点只是在AVL树的基础上增加了颜色的定义(这里我们还是使用key-value模型的二叉搜索树),其中颜色用枚举表示:
enum Color
RED,
BLACK
;
template<class K, class V>
struct RBTreeNode
RBTreeNode<K, V> _left;//结点的左孩子
RBTreeNode<K, V> _right;//结点的右孩子
RBTreeNode<K, V> _parent;//结点的双亲
pair<K, V>_kv;
Color _color;//该结点的颜色
RBTreeNode(const pair<K,V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_color(RED)//默认红色
;
在这里结点颜色默认设置为红色,是为了方便后续插入,在保证红黑树性质的情况下,通过插入红色结点可以更好的保证性质四不被破坏,而如果插入黑色结点,那么保证每条路径的黑色结点数量相等操作将非常繁琐。
📕红黑树的结构
template <class K, class V>
class RBTree
public:
//typedef RBTreeNode<T> Node;
typedef RBTreeNode<K,V> Node;
RBTree()
:_root(nullptr)
private:
Node* _root;
;
📕红黑树的插入(important!!!)
先抛出一个问题,为什么刚刚结点定义时选择结点默认颜色为红色?如何设置成黑色会如何?
如果选择插入黑结点,那1条路径上就多了1个黑结点,破坏了性质4,代价很大。插入红结点,如果它的父亲结点是黑色则不用调整,它的父亲是红色那我们在进行后序的处理。
总结一下,其实无论选红还是黑,后续都要再处理,但是处理的代价不一样:
1.插入黑色结点一定破坏性质4,调整起来会很麻烦
2.插入红结点不一定破坏红黑树的性质,它的父亲结点是红色才进行调整,比插入黑结点调整起来方便。
🌕1、寻找要插入的位置
若key值大于当前结点的key值,则向右寻找;若小于,则向左寻找;若相等,说明数据冗余,返回false。
🌕2.判断是否符合红黑树的规则
由于我们产生新结点时将其颜色设置为红色,而每条路径上黑色结点数量相等这条规则就交给调整时保证。
故判断是否为红黑树就转换为判断是否存在连续红色的结点。
- 对于新插入的结点,若其父亲颜色为黑色,则满足红黑树的规则,无需调整。
- 而若其父亲颜色为红色,则规则被破坏,需要调整。
🌕3.破坏红黑树规则的情况,需要调整
红黑树调整主要看叔叔结点,在规则被破坏的前提下, 总共存在三种情况:1.只需变色;2.单旋+变色;3.双旋+变色。
以下用p来代表parent结点,c代表cur为新增结点,g代表grandparent结点,u代表uncle结点。旋转的细节这里不再细讲,可以参考前面的AVL树的四种旋转实现
🍁情况一
cur为红, p为红, g为黑, u存在且为红
调整方式:
此时只需变色,将parent和uncle变为黑色,grandparent变为红色,同时,因为祖父之上可能还有其他节点,还需要从祖父g的位置继续向上调节。
🍁情况二
cur在p的左边,p为红,g为黑,u存在为黑色/u不存在 (孩子与父亲呈直线状态)
分析:这里有两种情况
1.如果叔叔存在,则此时的孩子节点可能是下面的子树在进行变色处理时,将其从原本的黑色变为了红色。(否则不满足路径黑节点数量相同的性质)
2.如果叔叔不存在,则此时的孩子节点是刚插入进来的结点,因为不能有连续的红结点,所以孩子和父亲必须有一个是黑色,但是此时又不满足黑节点数量相同的性质。
调整方式:旋转+变色处理
如果父亲是祖父的左孩子,孩子是父亲的左孩子,此时祖父进行右单旋
如果父亲是祖父的右孩子,孩子是父亲的右孩子,此时祖父进行左单旋。旋转完后,p变为黑色,g变为红色
🍁情况三
cur在p的右边,p为红,g为黑,u存在为黑色/u不存在 (孩子与父亲呈折线状态)
调整方式:双旋 + 变色处理
如果父亲是祖父的左孩子,孩子是父亲的右孩子,父亲此时进行左单旋,孩子再进行右单旋。
如果父亲是祖父的右孩子,孩子是父亲的左孩子,父亲此时进行右单旋,孩子再进行左单旋。双旋之后,cur变为黑色,parent和grandparent为红色。
🌕4.插入操作代码实现
bool Insert(const std::pair<K, V>& data)
//按照二叉搜索树的规则先找到位置
//创建根节点
if (_root == nullptr)
_root = new Node(data, BLACK);
return true;
Node* cur = _root;
Node* parent = nullptr;
while (cur)
if (data.first > cur->_data.first)
parent = cur;
cur = cur->_right;
else if (data.first < cur->_data.first)
parent = cur;
cur = cur->_left;
else
return false;
//新插入节点为红色
cur = new Node(data, RED);
//保存插入的结点,因为后面会往上更新红黑树,所以cur可能会变化。
Node* newNode = cur;
//判断插入位置
if (cur->_data.first > parent->_data.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
//更新红黑树,如果父节点的颜色为黑,则说明满足条件,不需要处理,如果为红,则说明不满足,需要处理。
while (parent && parent->_color == RED)
Node* ppNode = parent->_parent;
//如果父节点为祖父的左子树
if (ppNode->_left == parent)
//此时判断叔叔节点的状态,红黑树状态取决于叔叔
Node* uncle = ppNode->_right;
//第一种情况,如果叔叔节点存在且为红,则直接把父亲和叔叔变黑,祖父节点边红即可。然后再从祖父的位置继续往上调整
if (uncle && uncle->_color == RED)
//变色
uncle->_color = parent->_color = BLACK;
ppNode->_color = RED;
//继续往上
cur = ppNode;
parent = cur->_parent;
/*
叔叔节点为黑或者不存在,此时有两种情况
情况二:cur为父节点的左子树,即直线状态。
情况三:cur为父节点的右子树,即折线状态。
情况二单旋一次后更改颜色即可完成
对于情况三,如果将其进行一次旋转后再稍微处理,就可以转换成情况二
*/
else
//因为这里的双旋和AVL不一样,可以不用处理平衡因子,所以如果为折线则先旋转处理后,将其转换为直线处理即可。
//第三种情况,折线状态,转换为直线状态处理
if (parent->_right == cur)
RotateL(parent);
//单旋后再交换节点,即可变成直线状态。
std::swap(parent, cur);
//处理第二种状态
RotateR(ppNode);
parent->_color = BLACK;
ppNode->_color = RED;
//处理完成
break;
//如果父亲为祖父的右子树
else
//此时叔叔为左子树。
Node* uncle = ppNode->_left;
if (uncle && uncle->_color == RED)
uncle->_color = parent->_color = BLACK;
ppNode->_color = RED;
cur = ppNode;
parent = cur->_parent;
else
if (parent->_left == cur)
RotateR(parent);
std::swap(cur, parent);
RotateL(ppNode);
ppNode->_color = RED;
parent->_color = BLACK;
break;
//为了防止不小心把根节点改为红色,最后手动改为黑色即可
_root->_color = BLACK;
return true;
📕红黑树的查找
和二叉搜索树一样,直接上代码,不再赘述。
bool Find(const std::pair<K, V>& data)
//根据二叉搜索树的性质,从根节点出发,比根节点大则查找右子树,比根节点小则查找左子树
Node* cur = _root;
while (cur)
//比根节点大则查找右子树
if (data.first > cur->_data.first)
cur = cur->_right;
//比根节点小则查找左子树
else if (data.first < cur->_data.first)
cur = cur->_left;
//相同则返回
else
return true;
//遍历完则说明查找不到,返回false
return false;
📕红黑树的验证
要验证是否为红黑树,只需要判断其是否满足这三个性质。
- 根节点必须为黑节点
- 不存在连续的红结点
- 从某一节点出发到其所有的叶子节点,其中经过的黑色节点数量相等
💡1.根节点为黑色
判断根节点的颜色,若根节点为红色,破坏规则二,返回false。
bool IsRBTree()
if (_root == nullptr)//空树也是红黑树
return true;
//检测性质二:根节点是黑色
if (_root->_color != BLACK)
cout << "违反规则二,根结点为红色" << endl;
return false;
//判断性质三、四
//...
💡2.不存在连续的两个红色结点
这里需要注意的是,若去判断红色结点的孩子结点是否为红色,则还需要判断孩子是否存在;因此改为判断红色结点的父亲是否为红色,这样就可以简化代码及操作。
bool Check_RED_RED(Node* root)
//由于在调用函数中已经检测了根节点的合法性
//故此处的root结点必不为红色
if (root == nullptr)
return true;
if (root->_color == RED && root->_parent->_color == RED)
cout << "违反规则三,有连续的红色结点" << endl;
return false;
return Check_RED_RED(root->_left) && Check_RED_RED(root->_right);
💡3.每条路径的黑色结点数量都相等
先计算一条路径的黑色结点数量,然后遍历其他各条路径,对比黑色结点的数量,若不相等,则返回false。
//benchMark:基准值
bool Check_BlackNum(Node* root, int benchMark, int blacknum)
if (root == nullptr)//到空结点,此时blacknum为该条路径的黑色结点数量
if (blacknum == benchMark)
return true;
else
return false;
if (root->_color == BLACK)
blacknum++;
return Check_BlackNum(root->_left, benchMark, blacknum);
💡4.整体代码
bool IsRBTree()
if (_root == nullptr)//空树也是红黑树
return true;
//检测性质二:根节点是黑色
if (_root->_color != BLACK)
cout << "违反规则二,根结点为红色" << endl;
return false;
//计算最左路径上黑色结点的数量作为基准值
int benchMark = 0;
Node* cur = _root;
while (cur)
if (cur->_color == BLACK)
benchMark++;
cur = cur->_left;
int blacknum = 0;
return Check_RED_RED(_root) && Check_BlackNum(_root, benchMark, blacknum);
⏰3.红黑树完整代码
向你抛出了一颗红黑树~
#pragma once
#include<iostream>
enum Color
BLACK,
RED,
;
template<class K, class V>
struct RBTreeNode
typedef RBTreeNode<K, V> Node;
RBTreeNode(const std::pair<K, V>& data = std::pair<K, V>(), const Color& color = RED)
: _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _data(data)
, _color(color)
Node* _left;
Node* _right;
Node* _parent;
std::pair<K, V> _data;
Color _color;
;
template<class K, class V>
class RBTree
public:
typedef RBTreeNode<K, V> Node;
RBTree()
: _root(nullptr)
~RBTree()
destory(_root);
void _InOrderTravel(Node* root) const
if (root == nullptr)
return;
_InOrderTravel(root->_left);
std::cout << root->_data.first << ':' << root->_data.second << std::endl;
_InOrderTravel(root->_right);
void InOrderTravel() const
_InOrderTravel(_root);
void destory(Node*& root)
Node* node = root;
if (!root)
return;
destory(node->_left);
destory(node->_right);
delete node;
node = nullptr;
//右旋
void RotateR(Node* parent)
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
//如果subLR存在,则让他的父节点指向parent。
if (subLR)
subLR->_parent = parent;
subL->_right = parent;
Node* ppNode = parent->_parent;
parent->_parent = subL;
//两种情况
//如果parent为根节点,则让subL成为新的根节点
if (parent == _root)
_root = subL;
subL->_parent = nullptr;
//如果不是根节点,则改变subL与其祖父节点的指向关系
else
if (ppNode->_left == parent)
ppNode->_left = subL;
else
ppNode->_right = subL;
subL->_parent = ppNode;
//左旋
void RotateL(Node* parent)
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
subR->_left = parent;
Node* ppNode = parent->_parent;
parent->_parent = subR;
if (parent == _root)
_root = subR;
subR->_parent = nullptr;
else
if (ppNode->_left == parent)
ppNode->_left = subR;
else
ppNode->_right = subR;
subR->_parent = ppNode;
bool Find(const std::pair<K, V>& data)
//根据二叉搜索树的性质,从根节点出发,比根节点大则查找右子树,比根节点小则查找左子树
Node* cur = _root;
while (cur)
//比根节点大则查找右子树
if (data.first > cur->_data.first)
cur = cur->_right;
//比根节点小则查找左子树
else if (data.first < cur->_data.first)
cur = cur->_left;
//相同则返回
else
return true;
//遍历完则说明查找不到,返回false
return false;
bool Insert(const std::pair<K, V>& data)
//按照二叉搜索树的规则先找到位置
//创建根节点
if (_root == nullptr)
_root = new Node(data, BLACK);
return true;
Node* cur = _root;
Node* parent = nullptr;
while (cur)
if (data.first > cur->_data.first)
parent = cur;
cur = cur->_right;
else if (data.first < cur->_data.first)
parent = cur;
cur = cur->_left;
else
return false;
//新插入节点为红色
cur = new Node(data, RED);
//保存插入的结点,因为后面会往上更新红黑树,所以cur可能会变化。
Node* newNode = cur;
//判断插入位置
if (cur->_data.first > parent->_data.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
//更新红黑树,如果父节点的颜色为黑,则说明满足条件,不需要处理,如果为红,则说明不满足,需要处理。
while (parent && parent->_color == RED)
Node* ppNode = parent->_parent;
//如果父节点为祖父的左子树
if (ppNode->_left == parent)
//此时判断叔叔节点的状态,红黑树状态取决于叔叔
Node* uncle = ppNode->_right;
//第一种情况,如果叔叔节点存在且为红,则直接把父亲和叔叔变黑,祖父节点边红即可。然后再从祖父的位置继续往上调整
if (uncle && uncle->_color == RED)
//变色
uncle->_color = parent->_color = BLACK;
ppNode->_color = RED;
//继续往上
cur = ppNode;
parent = cur->_parent;
/*
叔叔节点为黑或者不存在,此时有两种情况
情况二:cur为父节点的左子树,即直线状态。
情况三:cur为父节点的右子树,即折线状态。
情况二单旋一次后更改颜色即可完成
对于情况三,如果将其进行一次旋转后再稍微处理,就可以转换成情况二
*/
else
//因为这里的双旋和AVL不一样,可以不用处理平衡因子,所以如果为折线则先旋转处理后,将其转换为直线处理即可。
//第三种情况,折线状态,转换为直线状态处理
if (parent->_right == cur)
RotateL(parent);
//单旋后再交换节点,即可变成直线状态。
std::swap(parent, cur);
//处理第二种状态
RotateR(ppNode);
parent->_color = BLACK;
ppNode->_color = RED;
//处理完成
break;
//如果父亲为祖父的右子树
else
//此时叔叔为左子树。
Node* uncle = ppNode->_left;
if (uncle && uncle->_color == RED)
uncle->_color = parent->_color = BLACK;
ppNode->_color = RED;
cur = ppNode;
parent = cur->_parent;
else
if (parent->_left == cur)
RotateR(parent);
std::swap(cur, parent);
RotateL(ppNode);
ppNode->_color = RED;
parent->_color = BLACK;
break;
//为了防止不小心把根节点改为红色,最后手动改为黑色即可
_root->_color = BLACK;
return true;
/*
判断是否为红黑树,就是判断其是否满足红黑树的特性
特性: 1.根节点必须为黑节点
2.不存在连续的红结点
3.从某一节点出发到其所有的叶子节点,其中经过的黑色节点数量相等
*/
bool IsRBTree()
if (_root == nullptr)
//空树也是红黑树
return true;
//违反性质1
if (_root->_color != BLACK)
return false;
//获取从根节点出发的任意一条子路径的黑色节点数,这里选取最左子树。
Node* cur = _root;
size_t blackCount = 0;
size_t count = 0;
while (cur)
if (cur->_color == BLACK)
blackCount++;
cur = cur->_left;
//递归判断其他路径的黑色节点数
return _IsRBTree(_root, count, blackCount);
bool _IsRBTree(Node* root, size_t count, const size_t blackCount)
//此时说明已经走到叶子节点,判断黑色节点数是否相等,不相等则违反性质3
if (root == nullptr)
if (count != blackCount)
return false;
else
return true;
//如果没走完,就接着判断其他情况
//判断性质2,如果存在连续的红结点,则返回错误
Node* parent = root->_parent;
if (parent && root->_color == RED && parent->_color == RED)
return false;
//如果当前节点为黑色,则记录
if (root->_color == BLACK)
count++;
//接着递归判断当前节点的所有路径
return _IsRBTree(root->_left, count, blackCount) && _IsRBTree(root->_right, count, blackCount);
private:
Node* _root;
;
以上是关于手撕红黑树(Red-Black Tree)的主要内容,如果未能解决你的问题,请参考以下文章