红黑树的相关概念
概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
结构图
红黑树结构和普通的二叉树差不多,但是多了一个header,header是根节点的父节点,根节点又是header的父节点,而header的左右孩子分别是整个树的最左节点和最右节点,也就是整个树的最小值和最大值。
性质
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
和AVL树的对比
AVL树,是一个十分平衡的搜索二叉树,他的所有子树的左右高度差是不会超过1的,因此在插入和删除的时候,需要做很多的旋转操作,甚至每插入几个结点就要旋转,因此它的插入效率很低。换来的是高效的查询效率,因为它的高度是很平衡的,因此它的查找效率是最接近O(logN)的。
红黑树,也是一个平衡的搜索二叉树,但是没有AVL数平衡,红黑树的平衡是指:其最长路径中节点个数不会超过最短路径节点个数的两倍。红黑树也构成了一个比较平衡的二叉树,因此插入节点时候的效率会比AVL树效率高一些,因为它不需要做频繁的旋转操作,它是通过本身自带的颜色属性来控制各个子树的高度,来达到一个相对的平衡。查找效率也是趋近于O(logN),虽然没有AVL数的查找效率高,但是也是logN级别的,而且让插入时候的操作变的简单。
总结:红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logN ),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
红黑树的插入操作
红黑树是在二叉搜索树的基础,上加上其平衡限制条件,因此红黑树的插入可分为两步:
- 按照二叉搜索的树规则插入新节点:树的左边小于数的右边
- 检测新节点插入后,红黑树的性质是否造到破坏
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:
前提:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
情况一: cur为红,p为红,g为黑,u存在且为红
解决方式:
将p,u改为黑,g改为红,然后把g当成cur,继续向上调整
情况二: cur为红,p为红,g为黑,u不存在/u为黑
解决方式:
第一步
2.1 p为g的左孩子,cur为p的左孩子,则进行右单旋转
2.2 p为g的右孩子,cur为p的右孩子,则进行左单旋转
第二步
p、g变色–p变黑,g变红
情况三: cur为红,p为红,g为黑,u不存在/u为黑(情况三其实是变形的情况二,情况二是p和cur都在左边或者都在右边,情况三是p和cur不在一边)
解决方式:
第一步
3.1 p为g的左孩子,cur为p的右孩子,则针对p做左单旋转
3.2 p为g的右孩子,cur为p的左孩子,则针对p做右单旋转
第二步
这里你会发现第一步完成后的结构变成了情况二的结构,所以这里的执行情况二的步骤即可
插入操作的源码
下面我插入的值是一个键值对,这是为了模拟封装set和map而制作的数据结构,在这里先不介绍如何封装set和map
bool Insert(std::pair<K, V> data)
{
//说明还没有结点
if (_head->_parent == _head)
{
pNode root = new Node(data);
root->_colour = BLACK;
_head->_left = _head->_right = _head->_parent = root;
root->_parent = _head;
return true;
}
pNode cur = _head->_parent;
pNode parent = nullptr;
//找到合适的位置插入
while (cur)
{
parent = cur;
if (cur->_data.first > data.first)
{
cur = cur->_left;
}
else if (cur->_data.first < data.first)
{
cur = cur->_right;
}
else
{
return false;
}
}
cur = new Node(data);
//左边
if (parent->_data.first > data.first)
{
parent->_left = cur;
}
//右边
else
{
parent->_right = cur;
}
cur->_parent = parent;
//分情况调整
//如果父节点是黑色,则不需要调整
while (cur != _head->_parent && cur->_parent->_colour == RED)
{
parent = cur->_parent;
pNode gParent = parent->_parent;
pNode uncle = nullptr;
//获取叔叔结点
if (gParent->_left == parent)
{
uncle = gParent->_right;
}
else
{
uncle = gParent->_left;
}
//如果叔叔结点存在,而且颜色是红色:
//向上逐层更新,暂不需要旋转
if (uncle && uncle->_colour == RED)
{
//更新颜色
parent->_colour = BLACK;
uncle->_colour = BLACK;
gParent->_colour = RED;
//更新结点
cur = gParent;
}
//如果叔叔结点不存在或者颜色是黑色:
//则需要旋转
else
{
//如果cur是parent的左孩子、parent是gParent的左孩子 或者 cur是parent的右孩子、parent是gParent的右孩子:
//进行单旋
if ((gParent->_left == parent && parent->_left == cur) || (gParent->_right == parent && parent->_right == cur))
{
if (parent->_left == cur)
{
Rotate_Right(gParent);
}
else
{
Rotate_Left(gParent);
}
gParent->_colour = RED;
parent->_colour = BLACK;
break;
}
//如果三代的关系不是全是左或者全是右:
//双旋
else
{
//下面只做了单旋,第二次旋转放在下一次循环
//这是双旋的场景,但是旋一次后就变成了上面的单旋的场景了,所以cur不更新成gParent
if (parent->_right == cur)
{
Rotate_Left(parent);
//因为交换完之后parent在下面cur在上面
//所以要把两个指向更改一下
std::swap(cur, parent);
}
else
{
Rotate_Right(parent);
//因为交换完之后parent在下面cur在上面
//所以要把两个指向更改一下
std::swap(cur, parent);
}
}
}
}
//根始终是黑色的
_head->_parent->_colour = BLACK;
_head->_left = Left_Most();
_head->_right = Right_Most();
return true;
}