mystl项目地址为:https://github.com/Alinshans/MyTinySTL
原STL库十分庞大,父子类关系十分复杂。故借这个项目来研究原STL库的代码逻辑,对代码的理解都以注释的形式写在了注释里
参考博客: http://blog.csdn.net/v_JULY_v/article/details/6105630
疑问:红黑树插入节点的时候,有两种形式。一种是允许重复节点的插入,另外一种是不允许重复节点的插入。
判断节点是否重复是一个我没有看明白的地方。具体函数体现在get_insert_unique_pos()。
性质1. 结点是红色或黑色。
性质2. 根结点是黑色。
性质3. 所有叶子都是黑色。(叶子是NIL结点)
性质4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
性质5. 从任一节结点到其每个叶子的所有路径都包含相同数目的黑色结点。
//rb_tree_node_base中有如下数据
base_ptr parent; // 父节点 typedef rb_tree_node_base* base_ptr;也就是指针类型
base_ptr left; // 左子节点
base_ptr right; // 右子节点
color_type color; // 节点颜色
//真正的节点是rb_tree_node,继承自rb_tree_node_base
struct rb_tree_node :public rb_tree_node_base
{
......
T value; // 节点值
......
}
//到这里,真正的节点内总共五个变量
rb_tree_iterator_base继承自bidirectional_iterator_tag,是可向前向后的读写和输入的迭代器。
//此函数主要是有一个变量,两个函数,用来被继承
struct rb_tree_iterator_base
:public mystl::iterator
{ //STL的迭代器有五种类型。
base_ptr node; // 指向节点本身
// 使迭代器前进,也就是下一个节点
/*可以理解为:寻找节点的后继节点,我们先将整棵树进行一次中序遍历,得到一个序列。在此序列中,node的后继节点就是序列的下一个节点。可是在在原树中位置不是如此,需要分多种情况*/
void inc()
{
if (node->right != nullptr){ //存在右子树,那么后继就是右子树的最左下节点
node = rb_tree_min(node->right);//此函数来寻找本树的最左下节点
}
else{ // 如果没有右子节点
auto y = node->parent;
while (y->right == node){//进行上溯查找node的父节点,也就是找最高层的父节点
node = y;
y = y->parent;
}
if (node->right != y) // 应对“寻找根节点的下一节点,而根节点没有右子节点”的特殊情况
node = y;
}
}
// 使迭代器后退
void dec(){
//同inc逻辑
}
}
真正的迭代器rb_tree_iterator继承自rb_tree_iterator_base
struct rb_tree_iterator :public rb_tree_iterator_base
{
//主要内容是构造函数和重载运算符,如++(重载是调用了inc()函数)
}
rb_tree_min(NodePtr x)来寻找x的最左下节点
rb_tree_max(NodePtr x)来寻找x的最右下节点
还有一些变色的函数,很简单
template
NodePtr rb_tree_next(NodePtr node) noexcept //找前驱节点,有点像inc()函数
{
if (node->right != nullptr)
return rb_tree_min(node->right);
while (!rb_tree_is_lchild(node))//此函数判断本节点是不是某个父节点的左孩子,不知道特殊情况是什么
node = node->parent;
return node->parent;
}
树在经过左旋右旋之后,树的搜索性质保持不变,但树的红黑性质则被破坏了
/*---------------------------------------*\
| p p |
| / \ / \ |
| x d rotate left y d |
| / \ ===========> / \ |
| a y x c |
| / \ / \ |
| b c a b |
\*---------------------------------------*/
// 左旋,参数一为左旋点,参数二为根节点
/* 如上图,x-a和y-c是不需要变动的。
首先将x的右孩子修改为b,然后将b的父节点修改为x,最后修改x,y的父节点。
修改顺序可能造成成员丢失 */
template
void rb_tree_rotate_left(NodePtr x, NodePtr& root) noexcept
{
auto y = x->right; // y 为 x 的右子节点
x->right = y->left;
if (y->left != nullptr)/*y的左孩子如果为空,x的右孩子也为空。感觉可以不写这一句,不写这个if的只是重新给空指针又赋值一次空指针*/
y->left->parent = x;
y->parent = x->parent;
if (x == root){ // 如果 x 为根节点,让 y 顶替 x 成为根节点
root = y;
}
else if (rb_tree_is_lchild(x)){ // 如果 x 是左子节点
x->parent->left = y;
}
else{ // 如果 x 是右子节点
x->parent->right = y;
}
// 调整 x 与 y 的关系
y->left = x;
x->parent = y;
}
右旋同理。
插入节点共有五种情况:
1:根节点
2:父黑
3:父红叔红
4:父红叔黑NULL,父旋
5:父红叔黑NULL,祖父旋
在旋转的过程中,处理3后,情况变为4,处理4后情况变为5,处理5后,达到平衡。
具体解释:
// 插入节点后使 rb tree 重新平衡,参数一为新增节点,参数二为根节点
//
// case 1: 新增节点位于根节点,令新增节点为黑
// case 2: 新增节点的父节点为黑,没有破坏平衡,直接返回
// case 3: 父节点和叔叔;节点都为红,令父节点和叔叔节点为黑,祖父节点为红,
// 然后令祖父节点为当前节点,继续处理
// case 4: 父节点为红,叔叔节点为 NIL 或黑色,父节点为左(右)孩子,当前节点为右(左)孩子,
// 让父节点成为当前节点,再以当前节点为支点左(右)旋
// case 5: 父节点为红,叔叔节点为 NIL 或黑色,父节点为左(右)孩子,当前节点为左(右)孩子,
// 让父节点变为黑色,祖父节点变为红色,以祖父节点为支点右(左)旋
// https://blog.csdn.net/v_JULY_v/article/details/6105630 参考地址
template
void rb_tree_insert_rebalance(NodePtr x, NodePtr& root) noexcept
{
rb_tree_set_red(x); // 新增节点规定为红色
while (x != root && rb_tree_is_red(x->parent))
{
if (rb_tree_is_lchild(x->parent)){ // 如果父节点是左子节点
auto uncle = x->parent->parent->right; //叔叔节点
if (uncle != nullptr && rb_tree_is_red(uncle)) { // case 3: 父节点和叔叔节点都为红
rb_tree_set_black(x->parent);
rb_tree_set_black(uncle);//父节点和叔节点都变为黑色
x = x->parent->parent;
rb_tree_set_red(x);//祖父节点为红色。继续处理体现在:x已经变为祖父节点且继续执行while上
}
else{ // 无叔叔节点或叔叔节点为黑
if (!rb_tree_is_lchild(x)){ // case 4: 当前节点 x 为右子节点
x = x->parent;
rb_tree_rotate_left(x, root); //以x的父节点为支点左旋
}
// 都转换成 case 5: 当前节点为左子节点
rb_tree_set_black(x->parent);
rb_tree_set_red(x->parent->parent);
rb_tree_rotate_right(x->parent->parent, root);
break;
}
}
else {// 如果父节点是右子节点,对称处理
auto uncle = x->parent->parent->left;
if (uncle != nullptr && rb_tree_is_red(uncle)) { // case 3: 父节点和叔叔节点都为红
rb_tree_set_black(x->parent);
rb_tree_set_black(uncle);
x = x->parent->parent;
rb_tree_set_red(x);
// 此时祖父节点为红,可能会破坏红黑树的性质,令当前节点为祖父节点,继续处理
}
else { // 无叔叔节点或叔叔节点为黑
if (rb_tree_is_lchild(x)) { // case 4: 当前节点 x 为左子节点
x = x->parent;
rb_tree_rotate_right(x, root);
}
// 都转换成 case 5: 当前节点为左子节点
rb_tree_set_black(x->parent);
rb_tree_set_red(x->parent->parent);
rb_tree_rotate_left(x->parent->parent, root);
break;//父节点变为黑色,祖父节点变为红色,以祖父节点为支点右旋
}
}
}
rb_tree_set_black(root); // 根节点永远为黑
}
删除节点分两个步骤进行:一是直接删除,二是调整红黑树(因为删除节点后,原红黑树可能发生改变)
删除z:
1,z是叶节点:直接删除
2,z只有一个儿子:那么将z的儿子取代z的位置
3,z左右孩子都有:左孩子的最右下节点(前驱)替代z,或者右孩子的最左下(后继)节点替代z。
调整:
1,如果删除的是红色节点,那么原红黑树的性质依旧保持,此时不用做修正操作。删红不变
2,如果删除的节点是黑色节点,原红黑树的性质可能会被改变,我们要对其做修正操作。 如果只是根节点,什么都不用做。
2.1,如果删除节点不是树唯一节点,那么删除节点的那一个支的到各叶节点的黑色节点数会发生变化,此时性质5被破坏。
2.2,如果被删节点的唯一非空子节点是红色,而被删节点的父节点也是红色,那么性质4被破坏。
2.3,如果被删节点是根节点,而它的唯一非空子节点是红色,则删除后新根节点将变成红色,违背性质2。
我们从被删节点后来顶替它的那个节点开始调整(即代码中的x节点)
此区域信息收集于博客,有较大修改
case 1:兄红。当前节点是黑且兄弟节点为红色。
解法:把父节点染成红色,把兄弟结点染成黑色,之后重新进入算法。
此变换后原红黑树性质5不变,而把问题转化为兄弟节点为黑色的情况
case 2:兄黑兄两儿黑。当前节点是黑且兄弟是黑色且兄弟节点的两个子节点全为黑色。
解法:令兄弟节点为红,父节点成为当前节点,重新进入算法。
case 3:兄黑兄左子红右子黑。
解法:把兄弟结点染红,兄弟左子节点染黑,之后再在兄弟节点为支点解右旋,之后重新进入算法。
case 4:兄黑兄左子任意,右子红。
解法:把兄弟节点染成当前节点父节点的颜色,把当前节点父节点染成黑色,兄弟节点右子染成黑色,之后以当前节点的父节点为支点进行左旋,此时算法结束,红黑树所有性质调整正确。
// 删除节点后使 rb tree重新平衡,参数一为要删除的节点,参数二为根节点,参数三为最小节点,参数四为最大节点
template
NodePtr rb_tree_erase_rebalance(NodePtr z, NodePtr& root, NodePtr& leftmost,
NodePtr&rightmost)
{
// y 是可能的替换节点,指向最终要删除的节点
auto y = (z->left == nullptr || z->right == nullptr) ? z : rb_tree_next(z);
// x 是 y 的一个独子节点或 NIL 节点
auto x = y->left != nullptr ? y->left : y->right;
// xp 为 x 的父节点
NodePtr xp = nullptr;
......//这里主要是删除节点z
// 此时,y 指向要删除的节点,x 为替代节点,从 x 节点开始调整。
// 如果删除的节点为红色,树的性质没有被破坏,否则按照以下情况调整(x 为左子节点为例):
// case 1: 兄弟节点为红色,令父节点为红,兄弟节点为黑,以父亲作为支点进行左(右)旋,继续处理
// case 2: 兄弟节点为黑色,且两个子节点都为黑色或 NIL,令兄弟节点为红,父节点成为当前节点,继续处理
// case 3: 兄弟节点为黑色,左子节点为红色或 NIL,右子节点为黑色或 NIL,
// 令兄弟节点为红,兄弟节点的左子节点为黑,以兄弟节点为支点右(左)旋,继续处理
// case 4: 兄弟节点为黑色,右子节点为红色,令兄弟节点为父节点的颜色,父节点为黑色,兄弟节点的右子节点
// 为黑色,以父节点为支点左(右)旋,树的性质调整完成,算法结束
if (!rb_tree_is_red(y))
{ // x 为黑色时,调整,否则直接将 x 变为黑色即可
while (x != root && (x == nullptr || !rb_tree_is_red(x)))
{
if (x == xp->left)
{ // 如果 x 为左子节点
auto brother = xp->right;
if (rb_tree_is_red(brother))
{ // case 1
rb_tree_set_black(brother);
rb_tree_set_red(xp);
rb_tree_rotate_left(xp, root);
brother = xp->right;
}
// case 1 转为为了 case 2、3、4 中的一种
if ((brother->left == nullptr || !rb_tree_is_red(brother->left)) &&
(brother->right == nullptr || !rb_tree_is_red(brother->right)))
{ // case 2
rb_tree_set_red(brother);
x = xp;
xp = xp->parent;
}
else
{
if (brother->right == nullptr || !rb_tree_is_red(brother->right))
{ // case 3
if (brother->left != nullptr)
rb_tree_set_black(brother->left);
rb_tree_set_red(brother);
rb_tree_rotate_right(brother, root);
brother = xp->right;
}
// 转为 case 4
brother->color = xp->color;
rb_tree_set_black(xp);
if (brother->right != nullptr)
rb_tree_set_black(brother->right);
rb_tree_rotate_left(xp, root);
break;
}
}
else {// x 为右子节点,对称处理
......
}
}
if (x != nullptr)
rb_tree_set_black(x);
}
return y;
}
在类rb_tree中,有三个数据
private:
// 用以下三个数据表现 rb tree
base_ptr header_; // 特殊节点,与根节点互为对方的父节点
size_type node_count_; // 节点数,在empty()和size()起到重要作用
key_compare key_comp_; // 节点键值比较的准则
// 以下三个函数用于取得根节点,最小节点和最大节点
base_ptr& root() const { return header_->parent; }
base_ptr& leftmost() const { return header_->left; }
base_ptr& rightmost() const { return header_->right; }
//这里的header_的左孩子是最小节点,右孩子是最大节点,父节点指向root
析构函数:
~rb_tree() { clear(); } //调用clear(),clear()中调用erase_since()递归的删除每个节点
查找函数:
// 查找键值为 k 的节点,返回指向它的迭代器
find(const key_type& key)
{
// key 小于等于 x 键值,向左走
// key 大于 x 键值,向右走
}
插入函数:
插入的函数有emplace,insert相关的函数,insert又是调用的emplace,所以说,实际上是emplace。
插入有两种类型的函数,分别是emplace_unique(不允许键值重复),emplace_multi(允许键值重复)
// 就地插入元素,键值允许重复
//get_insert_multi_pos()返回值是个pair:第一个元素是rb-tree迭代器指向新增结点;第二个表示成功与否
emplace_multi(Args&& ...args)
{
THROW_LENGTH_ERROR_IF(node_count_ > max_size() - 1, "rb_tree's size too big");
node_ptr np = create_node(mystl::forward(args)...); //创建一个节点
auto res = get_insert_multi_pos(value_traits::get_key(np->value));//返回应该插入的位置
return insert_node_at(res.first, np, res.second); //插入节点
}
// 在 x 节点处插入新的节点
// x 为插入点的父节点, node 为要插入的节点,add_to_left 表示是否在左边插入
insert_node_at(base_ptr x, node_ptr node, bool add_to_left) //1560
{
...... // 加入节点,维护header_等的三个数据
rb_tree_insert_rebalance(base_node, root()); //调整结构
++node_count_;
return iterator(node);
}
//不允许键值重复
emplace_unique(Args&& ...args)
{
......
auto res = get_insert_unique_pos(value_traits::get_key(np->value));
/*不允许键值的重复,主要是靠此函数来实现的*/
......
}
// get_insert_unique_pos 函数
rb_tree::get_insert_unique_pos(const key_type& key)
{ /** 返回一个pair,嵌套结构
/* 第一个值为一个 pair,包含插入点的父节点和一个 bool 表示是否在左边插入,
/* 第二个值为一个 bool,表示是否插入成功
*/
......
//x是根节点,y是head_,add_to_left = true
while (x != nullptr) { //此循环寻找插入节点的位置
y = x;
add_to_left = key_comp_(key, value_traits::get_key(x->get_node_ptr()->value));
x = add_to_left ? x->left : x->right;
}
iterator j = iterator(y); // 此时 y 为插入点的父节点
if (add_to_left){ //在左边添加
if (y == header_ || j == begin()){ // 如果树为空树或插入点在最左节点处,肯定可以插入新的节点
return mystl::make_pair(mystl::make_pair(y, true), true);
}
else{ // 否则,如果存在重复节点,那么 --j 就是重复的值
/* 没看懂,大概意思是说:迭代器已经重载了--,那么--j就是j的直接前驱。如果存在一样的数值,那么这个数值就是直接前驱。具体为什么不知道 */
--j;
}
}
if (key_comp_(value_traits::get_key(*j), key)) { // 表明新节点没有重复
return mystl::make_pair(mystl::make_pair(y, add_to_left), true);
}
// 进行至此,表示新节点与现有节点键值重复
return mystl::make_pair(mystl::make_pair(y, add_to_left), false);
}
另外,
emplace_multi_use_hint()就地插入元素,键值允许重复,当 hint 位置与插入位置接近时,插入操作的时间复杂度可以降低;
emplace_unique_use_hint()就地插入元素,键值不允许重复,当 hint 位置与插入位置接近时,插入操作的时间复杂度可以降低
键值:
底层使用pair
本文只是自己的理解,仅供学习。
如有错误,请指出。