红黑树简介
二叉搜索树讲解和实现 :二叉搜索树 - 掘金 (juejin.cn)
AVL树讲解和实现 :AVL树 C++实现 - 掘金 (juejin.cn)
2-3-4树讲解和实现 :2-3-4树 C++实现 - 掘金 (juejin.cn)
由于2-3-4树结构较为复杂,有些结点会有多个元素/多个孩子结点,
于是有人用2-3-4树改进出了红黑树,红黑树原理就是2-3-4树
不同的是 红黑树是近似平衡搜索二叉树.
--红黑树的一些规则
先简单看看,后面可以用2-3-4树推导
( 1 ) 每个结点是黑色或红色.
( 2 ) 根结点是黑色.
( 3 ) 一条路径上没有连续的红色结点 / 红色结点的孩子结点一定是黑色结点.
( 4 ) 叶子结点(这里的叶子结点指的是空结点/NIL结点)是黑色
( 5 ) 从任一结点到其每个叶子结点的所有路径都包含相同数目的黑色结点
--红黑树如何保证近似平衡
只要满足红黑树的以上规则,就能保证 从根到叶子结点的每条路径,
最长路径不超过最短路径的2倍,从而保证近似平衡.
2-3-4树与红黑树的转换
--2/3/4结点转换成红黑树的结点
2-3-4树里有2结点/3结点/4结点,它是怎么统一转换成 一个结点只有一个元素的红黑树呢?
( 1 ) 为了把 3/4结点 都能转换成 键值只有1个的结点,我们必须选出一个"代表",
其它元素虽然也单独作为一个结点,但它们是这个"代表"的"附属".
4结点,把中间的元素选出来作"老大",其它两个元素当"跟班"
"跟班"的父亲结点是"老大",合理

3结点,随便一个元素都可以作"老大",另一个元素当"跟班" 满足搜索二叉树的性质即可
"跟班"的父亲结点是"老大",合理


2结点,自己就是"老大"

( 2 ) 为了区分转换过来的【"代表"结点】和【"附属"结点】,
我们把"代表"结点用黑色标识,把"附属"结点用红色标识,这就是红黑树.
( 3 ) 如果3/4结点有孩子结点,如何转换?
核心:最大限度把孩子结点/子树 交给 "附属结点"管


( 4 ) 实例:将下面一棵2-3-4树转换成红黑树.
3结点有两种选"代表"的方法,所以一棵2-3-4树可以对应多棵红黑树.
下面转换成的红黑树只是其中一种.


--红黑树规则推导
牢记:红黑树就是一棵变形的2-3-4树
为什么 一个结点是黑色或红色
把2-3-4树的3/4结点转换成红黑树的结点,需要提取一个元素作为"代表",其它作为"附属".
"代表结点"为黑色,"附属结点"为红色.
为什么 根是黑色
首先,2-3-4树的根一定是2/3/4结点.(空树特殊)
第二,2结点自己就是"代表",为黑色结点;
3结点和4结点都是把"代表"提取上去,作为"附属"的父亲结点.
因此根结点一定是黑色的.
为什么 一条路径下没有连续的红色结点
首先,红色结点一定是"跟班","跟班"的父亲结点一定是"老大",
即红色结点的父亲结点一定是黑色结点.
为什么 空结点(NIL结点)是黑色
为了配合规则3,如果空结点是红色,
一条路径的最后一个非空结点也是红色,那就和规则3矛盾,没什么实际意义.
为什么 从任一结点到其每个叶子结点的所有路径都包含相同数目的黑色结点
( 1 ) 2-3-4树的结点只有2/3/4结点,
并且每个结点转换成红黑树结点时,都会有一个"代表"
( 2 ) 2-3-4树的每个叶子结点一定在同一高度中,
即2-3-4树的每个结点到叶子结点,每条路径上的结点数目一定相同.
如之前的图所示:

红黑树插入
--成员属性
在2-3-4树中,插入一个元素,优先合并.(即优先作为"附属结点")
因此在红黑树中,插入一个结点,都默认是红色结点,
于是新建一个结点,默认是红色结点.
arduino
#define RED 0
#define BLACK 1
//定义红黑树的结点
template<class K, class V>
struct RBTreeNode
{
RBTreeNode(const pair<K,V>& kv)
:_kv(kv)
{}
RBTreeNode*_parent = nullptr;
RBTreeNode* _left = nullptr;
RBTreeNode* _right = nullptr;
pair<K, V> _kv;
//2-3-4树里新插入的元素优先作为"附属元素" 和 其它结点合并,
//因此红黑树中新插入的结点优先是红色
bool _col = RED;
};
arduino
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K,V> Node;
private:
Node* _root = nullptr;
}
--直接插入
空树,插入后,将根结点置为黑色
ini
//空树
if (_root == nullptr)
{
//空树
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
}
找到插入位置,直接插入
ini
//先找到插入位置,将元素插入
Node* cur = _root;
Node* parent = nullptr;
while (cur != nullptr)
{
if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else//树里已经有相等的key
{
cur->_kv.second = kv.second;
return true;
}
}
//正式插入
Node* newNode = new Node(kv);
if (kv.first > parent->_kv.first)
parent->_right = newNode;
else
parent->_left = newNode;
//注意要更新newNode的父亲结点
newNode->_parent = parent;
--插入后的调整
红黑树的插入可以等价转换成 2-3-4树 的插入.
以下面的红黑树和它对应的2-3-4树为例

2-3-4树中,元素插入在2结点,2结点变成3结点.
即红黑树中,插入一个红色结点,父亲结点是黑色且没有其它孩子
例:插入一个元素3.5,2-3-4树中(3)结点变成了(3 3.5)结点,即由2结点变成了3结点.
红黑树中插入在没有孩子的黑色结点下面.

2-3-4树中,元素插入在3结点,3结点变成4结点
对应有6种情况,有4种需要调整
以下图为例:

插入一个2.5,2-3-4树中是插入在该3结点元素左侧,成为4结点
红黑树是插入在黑色结点下面,不需要调整

插入一个6,2-3-4树中是插入在3结点元素中间,成为4结点;
红黑树是插入在红色结点9左边,需要旋转+变色调整.
通过2-3-4树4结点的最终形态,我们就能得出 旋转+变色 要达到的效果.

插入一个9.5,2-3-4树是插入在3结点元素最右边,成为4结点;
红黑树插入在红色结点9右边,需要旋转+变色调整
通过2-3-4树4结点的最终形态,我们就能得出 旋转+变色 要达到的效果.

左倾:(代码注释里提到)

右倾:

判断叔叔结点颜色,是用来判断新插入的元素是否和4结点合并。
ini
Node* parent = newNode->_parent;
Node* grandParent = parent->_parent;
Node* uncle = nullptr;
if (grandParent != nullptr)
{
if (grandParent->_left == parent)
uncle = grandParent->_right;
else
uncle = grandParent->_left;
}
//1 与2-3-4树的2结点合并
if (parent->_col == BLACK)
return;
//2 与2-3-4树的3结点合并(有6种情况,只需要处理4种)
if (parent->_col == RED && (uncle == nullptr || uncle->_col == BLACK))
{
//2-3-4树的3结点,转换成红黑树结点,左倾
//插入元素插入在3结点元素的最左边位置
if (parent->_left == newNode && grandParent->_left == parent)
{
rightRotate(grandParent);
parent->_col = BLACK;
grandParent->_col = RED;
newNode->_col = RED;
}
//2-3-4树的3结点,转换成红黑树结点,左倾
//新元素插入在3结点元素的中间位置
else if (parent->_right == newNode && grandParent->_left == parent)
{
leftRotate(parent);
rightRotate(grandParent);
newNode->_col = BLACK;
parent->_col = RED;
grandParent->_col = RED;
}
//2-3-4树的3结点,转换成红黑树结点,右倾
//新元素插入在3结点元素的最右边位置
else if (parent->_right == newNode && grandParent->_right == parent)
{
leftRotate(grandParent);
parent->_col = BLACK;
grandParent->_col = RED;
newNode->_col = RED;
}
//2-3-4树的3结点,转换成红黑树结点,右倾
//新元素插入在3结点元素的中间位置
else if (parent->_left == newNode && grandParent->_right == parent)
{
rightRotate(parent);
leftRotate(grandParent);
newNode->_col = BLACK;
parent->_col = RED;
grandParent->_col = RED;
}
return;
}
2-3-4树中,元素插入在4结点
插入位置有4种,最后都会发生裂变.
2-3-4树的4结点转化成红黑树的结点后,新插入的结点一定是3的孩子或9的孩子,
都需要调整.但调整的方案都是一致的.

2-3-4树,结点的裂变 对应 红黑树的调整
插入1,我们固定让原来的中间元素5裂变上去,找父亲结点合并.
然后左边元素 和 右边元素 各自新建一个结点,作为父亲结点的新孩子.

因为中间元素上去找父亲结点合并,所以它在红黑树里就成了附属结点(红色结点).
其它结点按裂变后的情况,调整颜色即可.
ini
//3 和2-3-4树的4结点合并(有4种情况,但是处理方式相同),都是裂变
//这里父亲结点为红色,叔叔结点必须是红色
if (parent->_col == RED && uncle->_col == RED && grandParent->_col == BLACK)
{
//变色,继续向上调整
parent->_col = BLACK;
uncle->_col = BLACK;
grandParent->_col = RED;
newNode = grandParent;
}
但是,在2-3-4树中,该中间元素裂变上去后,和父亲结点进行合并,
又会发生 2结点->3结点、3结点->4结点、4结点 + 1个元素需要裂变,
所以完整的插入后的调整,应该是一个循环.
ini
//插入newNode的调整
void insertAdjust(Node* newNode)
{
//父亲结点是黑色,不需要调整
if (newNode->_parent->_col == BLACK) return;
while (newNode != _root)
{
//这里parent不可能为空.
//确定父亲、祖父和叔叔结点
Node* parent = newNode->_parent;
Node* grandParent = parent->_parent;
Node* uncle = nullptr;
if (grandParent != nullptr)
{
if (grandParent->_left == parent)
uncle = grandParent->_right;
else
uncle = grandParent->_left;
}
//1 与2-3-4树的2结点合并
if (parent->_col == BLACK)
return;
//2 与2-3-4树的3结点合并(有6种情况,只需要处理4种)
if (parent->_col == RED && (uncle == nullptr || uncle->_col == BLACK))
{
//2-3-4树的3结点,转换成红黑树结点,左倾
//插入元素插入在3结点元素的最左边位置
if (parent->_left == newNode && grandParent->_left == parent)
{
rightRotate(grandParent);
parent->_col = BLACK;
grandParent->_col = RED;
newNode->_col = RED;
}
//2-3-4树的3结点,转换成红黑树结点,左倾
//新元素插入在3结点元素的中间位置
else if (parent->_right == newNode && grandParent->_left == parent)
{
leftRotate(parent);
rightRotate(grandParent);
newNode->_col = BLACK;
parent->_col = RED;
grandParent->_col = RED;
}
//2-3-4树的3结点,转换成红黑树结点,右倾
//新元素插入在3结点元素的最右边位置
else if (parent->_right == newNode && grandParent->_right == parent)
{
leftRotate(grandParent);
parent->_col = BLACK;
grandParent->_col = RED;
newNode->_col = RED;
}
//2-3-4树的3结点,转换成红黑树结点,右倾
//新元素插入在3结点元素的中间位置
else if (parent->_left == newNode && grandParent->_right == parent)
{
rightRotate(parent);
leftRotate(grandParent);
newNode->_col = BLACK;
parent->_col = RED;
grandParent->_col = RED;
}
return;
}
//3 和2-3-4树的4结点合并(有4种情况,但是处理方式相同),都是裂变
//这里父亲结点为红色,叔叔结点一定是红色
if (parent->_col == RED && uncle->_col == RED && grandParent->_col == BLACK)
{
//变色,继续向上调整
parent->_col = BLACK;
uncle->_col = BLACK;
grandParent->_col = RED;
newNode = grandParent;
}
}
//无论有没有调整到根,最好都要把_root置黑
_root->_col = BLACK;
}
红黑树删除
--直接删除
先找到要删除的结点,如果不是叶子结点,就用替换法转换成删除叶子结点.
锁定要删除的结点以后,先不要删除,用来判断删除后的调整情况.
arduino
//返回真正要删除的结点
Node* ordinaryErase(const K& key)
{
//找到要删除的结点
Node* del = find(key);
if (del == nullptr) return nullptr;
//如果有两个孩子结点,替换法转化成删除左子树的最大结点
if (del->_left != nullptr && del->_right != nullptr)
{
Node* max = del->_left;
while (max->_right != nullptr)
max = max->_right;
//此时max就是可以替代del进行删除的结点
del->_kv = max->_kv;
//让del指向真正要删除的结点
del = max;
}
return del;
}
//查找键值为key的结点
Node* find(const K& key)
{
Node* cur = _root;
while (cur != nullptr)
{
if (key < cur->_kv.first)
cur = cur->_left;
else if (key > cur->_kv.first)
cur = cur->_right;
else
return cur;
}
return nullptr;
}
删除的大致思路:
arduino
//删除实现
bool erase(const K& key)
{
if (_root == nullptr) return false;
//得到真正要删除的结点
Node* cur = ordinaryErase(key);
if (cur == nullptr)
return false;
//cur就是删除的结点,判断删除情况
eraseAdjust(cur);
return true;
}
--删除后的调整
如果删除的结点是红色结点,不会影响整棵红黑树,不需要调整。
当删除黑色结点时,可以把红黑树的删除转换成2-3-4树的删除,
即删除3结点里的元素、删除2结点里的元素.
删除3结点里的元素
( 1 ) 删除的是红色结点,不需要调整
ini
//要删除的结点没有找到
if (cur == nullptr)
return;
// 删除的结点是红色结点,不需要调整
if (cur->_col == RED)
{
deleteNode(cur);
return;
}
( 2 ) 删除的是黑色结点1,直接删除,同时把它红色的孩子结点变黑.

ini
//删除的结点是黑色结点
//(1) 删除的结点是3结点里的"代表"元素
//(2) 删除的结点是2结点
//情况(1),直接删除,给孩子结点变黑色
Node* child = cur->_left;
if (child == nullptr)
child = cur->_right;
if (cur->_col == BLACK && child != nullptr && child->_col == RED)
{
deleteNode(cur);
child->_col = BLACK;
return;
}
删除2结点里的元素
( 1 ) 在2-3-4树中,相邻兄弟结点有多余的元素,需要通过父亲结点向兄弟结点借元素
在红黑树中就是旋转+变色。以下图为例:【删除黑色结点12】

在2-3-4树的删除:

在红黑树的删除:
以删除结点是左孩子为例:【若删除结点是为右孩子同理】
情况1: 相邻兄弟结点是黑色,且有多余的红色孩子结点

( A ) 若黑色兄弟结点23有一个红色右孩子结点,直接左旋父亲结点的子树即可,
同时根据对应2-3-4树删除后的情况进行变色。

( B ) 若黑色兄弟结点23同时有两个红色孩子结点,直接左旋父亲结点的子树
等价于在2-3-4树中借兄弟结点的两个元素。

( C ) 若黑色兄弟结点只有一个红色左孩子结点,需要双旋。

情况2: 相邻兄弟结点是红色,说明该兄弟结点不是真正的兄弟结点,
要找到转换成2-3-4树后的真兄弟结点。例:

直接左旋变色即可转换成第一种情况。
( 2 ) 在2-3-4树中,相邻兄弟结点没有多余的元素,
( A ) 父亲结点有多余的元素
那在红黑树中就是删除结点的父亲结点为红色。例:

在2-3-4树中,向父亲结点借元素,然后把删除元素后的结点 和 相邻兄弟结点合并:
对应红黑树的调整,将父亲结点变黑,兄弟结点变红即可.

( B ) 父亲结点没有多余的元素
在红黑树中就是删除结点的父亲结点为黑色。例:
在下图的2-3-4树中,删除元素11,
向父亲结点借元素,然后和相邻兄弟结点合并,
继续向上调整,直到parent结点的元素个数不为0.
2-3-4树还有一种删除方式更切合红黑树的删除。例:

相当于把借空元素的结点,继续当成删除结点,重复删除2结点的情况。
红黑树在旋转时,也能将A结点调整到对应位置。


将结点12当成删除的结点:
重复判断相邻兄弟结点是否有多余的红色孩子结点,父亲结点是否为红色结点,
都不满足继续将兄弟结点染红,将父亲结点作为删除结点继续向上调整。
整体调整的代码
ini
//删除cur的调整
void eraseAdjust(Node* cur)
{
//要删除的结点没有找到
if (cur == nullptr)
return;
// 删除的结点是红色结点,不需要调整
if (cur->_col == RED)
{
deleteNode(cur);
return;
}
//删除的结点是黑色结点
//(1) 删除的结点是3结点里的"代表"元素
//(2) 删除的结点是2结点
//情况(1),直接删除,给孩子结点变黑色
Node* child = cur->_left;
if (child == nullptr)
child = cur->_right;
if (cur->_col == BLACK && child != nullptr && child->_col == RED)
{
deleteNode(cur);
child->_col = BLACK;
return;
}
//情况(2),在2-3-4树中,2结点无法直接删除,需要向父亲结点拿元素覆盖删除
//然后父亲结点找cur的相邻兄弟拿元素(如果cur相邻兄弟有多余的元素) 【也可以说是删除结点直接向兄弟结点借】
//先保存要删除的结点
Node* del = cur;
while (cur != _root)
{
//(a) 兄弟结点有多余的孩子结点
//注意:转化成红黑树以后,需要找到"真正"的兄弟结点
Node* curBother = getTrueBother(cur);
Node* parent = cur->_parent;
if (parent->_left == cur)
{
//兄弟没得借
if (getColor(curBother->_left) == BLACK && getColor(curBother->_right) == BLACK)
{
//兄弟自损
curBother->_col = RED;
if (parent->_col == RED)
{
parent->_col = BLACK;
break;
}
cur = parent;
}
//兄弟有得借
//分两种小情况:兄弟结点是3结点或4结点
//如果兄弟结点有两个孩子/只有一个右孩子,直接借两个元素,剩下一个作为2结点
else if (getColor(curBother->_right) == RED)
{
Node* newParent = leftRotate(parent);
newParent->_col = parent->_col;
newParent->_left->_col = BLACK;
newParent->_right->_col = BLACK;
break;
}
else if (getColor(curBother->_left) == RED)
{
rightRotate(curBother);
Node* newParent = leftRotate(parent);
newParent->_col = parent->_col;
newParent->_right->_col = BLACK;
newParent->_left->_col = BLACK;
break;
}
}
else if (parent->_right == cur)
{
//兄弟结点没得借
if (getColor(curBother->_left) == BLACK && getColor(curBother->_right) == BLACK)
{
curBother->_col = RED;
if (parent->_col == RED)
{
parent->_col = BLACK;
break;
}
cur = parent;
}
else if (getColor(curBother->_left) == RED)
{
Node* newParent = rightRotate(parent);
newParent->_col = parent->_col;
newParent->_left->_col = BLACK;
newParent->_right->_col = BLACK;
break;
}
else if (getColor(curBother->_right) == RED)
{
leftRotate(curBother);
Node* newParent = rightRotate(parent);
newParent->_col = parent->_col;
newParent->_left->_col = BLACK;
newParent->_right->_col = BLACK;
break;
}
}
}
_root->_col = BLACK;
deleteNode(del);
}
删除叶子结点代码
ini
//删除结点【该结点最多只有1个孩子】
void deleteNode(Node* node)
{
assert(_root && node);
Node* parent = node->_parent;
//删除根结点
if (parent == nullptr)
{
if (node->_left == nullptr)
_root = node->_right;
else
_root = node->_left;
if(_root != nullptr)
_root->_parent = nullptr;
}
else//删除普通结点
{
if (parent->_left == node)
{
if (node->_left == nullptr)
{
parent->_left = node->_right;
if (node->_right != nullptr)
node->_right->_parent = parent;
}
else if(node->_right == nullptr)
{
parent->_left = node->_left;
if (node->_left != nullptr)
node->_left->_parent = parent;
}
}
else if (parent->_right == node)
{
if (node->_left == nullptr)
{
parent->_right = node->_right;
if (node->_right != nullptr)
node->_right->_parent = parent;
}
else if(node->_right == nullptr)
{
parent->_right = node->_left;
if (node->_left != nullptr)
node->_left->_parent = parent;
}
}
}
delete node;
}
求cur真正的兄弟结点
ini
//还原成2-3-4树后,cur真正的兄弟结点
Node* getTrueBother(Node* cur)
{
if (cur == nullptr || cur->_parent == nullptr)return nullptr;
Node* bother = getBother(cur);
Node* parent = cur->_parent;
if (bother == nullptr) return nullptr;
if (find(bother) == false)
{
cout << "bother异常!!!" << endl;
exit(-1);
}
if (bother->_col == RED)//不是真正的兄弟结点
{
if (parent->_left == cur)
{
Node* newParent = leftRotate(parent);
newParent->_col = BLACK;
newParent->_left->_col = RED;
}
else
{
Node* newParent = rightRotate(parent);
newParent->_col = BLACK;
newParent->_right->_col = RED;
}
bother = getBother(cur);
}
return bother;
}
判断是否为红黑树
非递归先序遍历这棵树,保证这棵树符合红黑树的5个规则:
【通过2-3-4树可以更好的理解这些规则】
( 1 ) 每个结点是黑色或红色.
( 2 ) 根结点是黑色.
( 3 ) 一条路径上没有连续的红色结点 / 红色结点的孩子结点一定是黑色结点.
( 4 ) 叶子结点(这里的叶子结点指的是空结点/NIL结点)是黑色
( 5 ) 从任一结点到其每个叶子结点的所有路径都包含相同数目的黑色结点
ini
//判断是否为红黑树
bool isRBTree()
{
//空树
if (_root == nullptr)return true;
//违反根是黑色结点性质
if (_root->_col == RED) return false;
stack<Node*> st;//保存左路结点
Node* cur = _root;//cur代表要访问的一整棵树
while (cur || !st.empty())
{
//访问左路结点
while (cur)
{
st.push(cur);
//存在连续的红色结点
if (cur->_col == RED && cur->_parent->_col == RED)
return false;
if (leavesToCurBlackNum(cur) == false)
return false;
cur = cur->_left;
}
//访问左路结点的右子树
Node* top = st.top();
st.pop();
cur = top->_right;
}
return true;
}
任意结点到叶子结点的每个路径,黑色结点数目是否相同【用到后序遍历】
ini
//当前结点cur到叶子结点的所有路径下,黑色结点数目是否相同
bool leavesToCurBlackNum(Node* cur)
{
//一棵二叉树拆分为 左路结点 和 左路结点的右子树
stack<Node*> st;
//保存 cur 到 最初的cur 路径,黑色结点的数目
int num = 0;
//保存 最初的cur 到 任意的叶子结点 的其中一条路径,黑色结点的数目
int flag = 0;
Node* pre = nullptr;
//cur代表要遍历的树
//栈里存储的是遍历完的左路结点,需要依次取栈顶的结点,遍历左路结点的右子树
while (cur || !st.empty())
{
//左路结点入栈
while (cur)
{
st.push(cur);
//cur 到 最初的cur的路径下,黑色结点数目为num
if (cur->_col == BLACK)
num++;
if (cur->_left == nullptr || cur->_right == nullptr)
{
//如果是叶子结点,记录该结点到最初cur结点的黑色结点数目
if (flag == 0)
flag = num;
else
if (flag != num)//有一条路径不相等
return false;
}
cur = cur->_left;
}
//访问左路结点的右子树
Node* top = st.top();
//右子树访问完成
if (top->_right == nullptr || top->_right == pre)
{
pre = top;
st.pop();
//如果右子树已经访问完成,那么下一个要访问的左路结点,该结点到最初的cur路径的黑色结点数目,不会包括top
if ( top->_col == BLACK)
--num;
}
else
{
cur = top->_right;
}
}
return true;
}