目录
一.什么是红黑树?
在学红黑树之前,我们需要回顾二叉树的知识,二叉树的基本概念
- 如果左子树不为空,则左子树上的所有值都小于根节点的值。
- 如果右子树不为空,则右子树上的所有值都大于根节点的值。
- 左右子树也必须满足二叉搜索树。
每一个节点的值都必须大于它的左子树中的所有节点,小于它的右子树的所有节点。
二叉树的查找的时间复杂度与它的高度有关。
一颗二叉搜索树的接近于完全二叉树,它的高度是logN,所以它的查找效率约是logN(如上图),比如我们要找8这个值,先跟5比较,比5大,到它的右子树查找,再跟7比较,比7大,则到7的右子树查找,在跟8比较,则找到了,如上过程中只需要3次对比即可。
但是单纯二叉搜索树很难保证是一颗完全二叉树,它也有极端情况,例如搜索二叉树它可能是一条路径,它的高度是N(N是节点的个数),此时的搜索效率就是O(N),如下,查找8的过程,需要比对6次。
而如下将的红黑色是尽可能的将一颗搜索二叉树维持成一颗完全二叉搜索树。
二. 红黑树的基本概念
红黑树是一颗特殊的二叉搜索树,它除了遵循二叉搜索树的规则外,还需要遵循以下规则:
- 每棵树只红色节点和黑色节点
- 根节点为黑色。
- 每条路径的黑色节点个数都是相同的。
- 不能出现连续的红色节点。
- 红黑树的路径是
由此我们可以推出红黑树的结论:
红黑树的最长路径不会超过最短路径的两倍(最长路径<=最短路径*2)为什么?
因为每条路径的黑色节点的个数都相同,不能出现连续的红色节点,最短路径上只有黑色节点,而最长路径上的黑色节点与最短路径的黑色节点数量相等,且它的红色节点与黑色节点的数量相等,它们之间交替连接。
如下:
上图中最短路径和最长路径的黑色节点的个数都为2,但是最长路径有两个红色节点,它并没有破坏红黑树的任意规则,如果最长路径再插入一个红色节点或者黑色节点,则会破坏红黑树的规则,则会发生旋转对其进行调整。
因此一颗红黑存在很多节点下,最短路径的高度约等于logN,查找效率大约是logN,最长路径约等于2logN,查找效率大约是2logN,所以红黑树的总体查找效率能够保证在logN.
三.红黑树的插入
红黑树新插入的节点总是红色,因为红色只影响当前的一条路径,影响较小,如果插入的是黑色节点,那么所有路径都会被影响,影响较大,为什么呢?
如下:
下面的红黑树中所有的路径都是两个黑色节点,如果此时往其中任意一条路径插入一个黑色几点,该条路径的黑色节点就是3个,那么就会破坏规则"每条路径的黑色节点个数都是相同的“,因此,所以所有的路径都会被影响到。
如果插入的节点是红色节点,那么就可能会受到影响,如果我们在往红黑树中插入一个值为9的节点,此时就会插入到10节点的左子树,此时不会破坏红黑的规则,如果是插入一个值为25的节点,那么它就会插入在24节点的右子树上,此时就会破坏规则"不能出现连续的红色节点”,但是只是影响到12-20-24-25这条路径,那么此时就需要进行调整,具体调整如下解释。
因此,红黑树插入红色节点影响是比较小的,有可能影响到一条路径,而如果插入的是黑色节点,它必定会影响到红黑的所有路径。
综上所述,红黑树新插入的节点为红色。
红黑树节点的构造
enum COLOR//节点的颜色
{
RED,
BLACK
};
template<class K, class V>
struct RBTReenode//红黑树的节点
{
RBTReenode(pair<K, V> kv)
:_parent(nullptr)
, _left(nullptr)
, _right(nullptr)
, _kv(kv)
{
}
//节点是三插链
RBTReenode* _parent;//节点的父亲节点
RBTReenode* _left;//节点的左子树
RBTReenode* _right;//节点的右子树
pair<K, V> _kv;//节点存储的值
COLOR _color = RED;//默认为红色
};
红黑树是三叉链结构,且默认的颜色是红色,且节点是三叉链,翻遍旋转
红黑树的插入
首先我们得认清楚一些概念,如下图;
新插入的节点是cur,它的父亲节点叫做parent,它的爷爷节点叫做pparent,它的叔叔节点叫做uncle。
因为插入的新节点默认是红色,所以我们插入后需要判断parent是否为红色。
1.parent是黑色
如果是parent是黑色,那么则不会破坏红黑树的规则(ps:cur为新插入节点),此时不需要进行调整。
如下: 当往红黑树中插入值为9的节点时,那么cur直到走到null时,在创建新节点给cur,此时的parent是黑色节点,因此是不会破坏红黑树的规则。
2.parent是红色
当parent是红色的时候,此时需要判断parent是位于pparent的哪颗子树,这样才能找出uncle是位于pparent中的哪颗子树,因为我们需要通过uncle的颜色来决定接下来的操作。
情况一:如果uncle存在且uncle为红色节点.如下;
此时将parent和uncle都变为黑色节点,pparent变为红色。(注意:parent为红,那么pparent必然存在且为黑,因为_root必须为黑色节点,所以parent不可能为根节点)。
如下:
调整颜色后,cur需要走到pparent上的位置,继续向上判断,因为pparent的父亲节点可能是红色节点,如果pparent的父亲节点是黑色,或者pparent是根节点,则不需要向上判断。
情况二:如果uncle不存在
- 如果parent位于pparent的左子树,且cur位于parent的左子树,pparent-parent-cur是 “ / ” 这种形状(如下),此时需要对pparent右单旋,旋转后将parent调整为黑色,pparent调整为红色。
- 如果parent位于pparent的左子树,且cur位于parent的右子树,pparent-parent-cur 呈现的是 “ < "这种形状,此时就需要先对parent进行左单旋,左单旋后的pparent-cur-parent呈现的是" / ”这种形状,然后再对pparent进行右单旋,右单旋后将cur颜色调整为黑色,再将pparent调整为红色
- 如果parent位于pparent的右子树,且cur位于parent的右子树,pparent-parent-cur 呈现的是 “ \ "这种形状,此时就需要对pparent进行左单旋,旋转后将pparent调整为红色,cur调整为黑色。
- 如果parent位于pparent的右子树,且cur位于parent的左子树,pparent-parent-cur 呈现的是 “ > "这种形状,则需要先对parent进行右单旋,旋转后pparent-cur-parent呈现的是" \ "这种形状,然后再对pparent进行左单旋,旋转后再将pparent调整为红色,cur调整为黑色。
情况三:uncle存在且为黑
uncle存在且为黑色,这种情况直接插入是不能出现的结果,它是需要从情况一"uncle存在且为红色"调整颜色变换过来的。如下:
- 通过情况一调整颜色后,cur走到pparent的位置,parent走到pparent的父亲节点,继续判断,此时的parent是红色,且uncle存在且为黑色,pparent-parent-cur呈现的是" / “这种形状,因此我们需要对pparent进行右单旋,单旋的过程如下详细解析。
- 如果pparent-parent-cur呈现的是" < "形状,则需要对parent先进行左单旋,旋转后的 pparent- cur-parent呈现的是" / "形状,再对pparent进行左单旋,旋转后将cur的颜色调整为黑色,将ppaent的颜色调整为红色。
- 如果parent位于pparent的右子树,且cur位于parent的右子树,pparent-parent-cur是 " \ "这种形状。此时对pparent进行左单旋,旋转后将parent的节点颜色变为黑色,将pparent节点颜色变为红色。 如下(省略情景一的变色)
- 如果parent位于pparent的右子树,且cur位于parent的左子树,即pparent-parent-cur为“ > "这种形状,此时需要对parent进行右单旋,然后对pparent进行左单旋,最后再将cur节点变为黑色,将pparent变为红色。
总结:
- uncle不存在和uncle存在且为黑这两种情景,如果它们的pparent-parent-cur是同一形状,则它们的旋转方式和调整颜色都是一样的,因此,我们可以将则两种情况看作成一种情况,如果我们手撕代码的时候,我们可以对照uncle不存在这种情况来进行写代码即可,因为它不需要对情景一进行变色,较为简单
- 无论哪种请景,只要红黑树旋转后,则这颗红黑树一定不违反红黑树中的任意规则,所以就不需要向上判断。
- 情景一变色后,则需要继续向上判断,则cur跳到pparent的位置,跳转后,如果parent或者pparent不存在,则不需要进行任何调整。
红黑树插入的整体逻辑
红黑树插入的代码
pair<Node*, bool> insert(pair<K, V> kv)
{
if (_root == nullptr)//根节点不存在
{
_root = new Node(kv);//直接插入到根节点中
_root->_color = BLACK;
return make_pair(_root, true);
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
//如果kv比当前节点的值要小,到它的左子树找
if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
//如果kv比当前节点的值要大,到它的右子树找
else if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
//如果相等,则不进行插入值
else
{
return make_pair(cur, false);
}
}
cur = new Node(kv);//创建新节点
//cur与parent互相连接
cur->_parent = parent;
if (parent->_kv.first > kv.first)
{
parent->_left = cur;
}
else
{
parent->_right = cur;
}
//前面都是二叉搜索树的插入
while (parent&&parent->parent)//parent为空,则说明为根节点,不需要往上判定
{
//parent为黑色节点
if (parent->_color == BLACK)
{
//直接返回
return make_pair(cur, true);
}
Node* pparent = parent->_parent;
if (pparent->_left == parent)//parent是pparent的左子树
{
Node* uncle = pparent->_right;
if (uncle && uncle->_color == RED)//uncle存在且为红色
{
uncle->_color = BLACK;
parent->_color = BLACK;
pparent->_color = RED;
cur = pparent;//向上更新
parent = cur->_parent;
}
else
{
if (parent->_left == cur)//cur是parent的左子树
{
RotateRight(pparent);
pparent->_color = RED;
parent->_color = BLACK;
}
else//cur是parent的右子树
{
//左右双旋
RotateLeft(parent);
RotateRight(pparent);
cur->_color = BLACK;
pparent->_color = RED;
}
break;
}
}
else//parent 是pparent的右子树
{
Node* uncle = pparent->_left;
if (uncle && uncle->_color == RED)//uncle存在且为红色
{
parent->_color = BLACK;
uncle->_color = BLACK;
pparent->_color = RED;
cur = pparent;
parent = cur->_parent;
}
else
{
if (parent->_right == cur)//cur位于parent的右子树
{
RotateLeft(pparent);//左单旋
pparent->_color = RED;
parent->_color = BLACK;
}
else if (parent->_left == cur)//cur位于parent的左子树
{
//右左双旋
RotateRight(parent);
RotateLeft(pparent);
pparent->_color = RED;
cur->_color = BLACK;
}
break;
}
}
}
_root->_color = BLACK;//最后将根节点都变为黑色
}
四.右单旋
parent是要旋转的节点,右单旋函数外的parent是需要进行判断节点。
在红黑树中,当左子树高度大于右子树的高度的二倍及以上时,就需要进行右单旋,即增加右子树的高度,减少左子树的高度。如下是红黑树需要进行有单旋的三种情景:
为了让旋转过程:
- parent的_left去连接subL的右子树subLR,如果subLR不为空,让subLR的_parent连接parent。
- 让subL的_right去连接parent,parent的_parent连接subL。(parent和subL一定存在,不需要判断)
- 最后让subL的_parent连接pparent,如果pparent不为空,判断parent位于pparent位于哪颗子树,如果parent是在pparent的左子树,那么pparent的_left去连接subL,如果parent是位于pparent的右子树,那么pparent的_right去连接subL.
- 如果pparent为空,则说明parent是根节点,旋转后的红黑树的根节点就变为subL.
总结:
总共有6次连接,parent与subLR进行互相连接,subL与parent进行互相连接,subL与pparent互相连接。其中subLR和pparent不一定存在,因此需要判断它们是否存在。
如果旋转的节点parent是根节点,则旋转后就需要将根节点设置为subL。
代码:
void RotateRight(Node* parent)
{
Node* pparent = parent->_parent;
Node* subL = parent->_left;
Node* subLR = subL->_right;
//parent与subLR的互相连接
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
//subL与parent互相连接
subL->_right = parent;
parent->_parent = subL;
//subL与pparent互相连接
subL->_parent = pparent;
if (pparent)//pparent不为NULL
{
if (pparent->_left == parent)//parent位于pparent的左子树
{
pparent->_left = subL;
}
else if (pparent->_right == parent)//parent位于pparent的右子树
{
pparent->_right = subL;
}
}
else//pparent为NULL
{
_root = subL;
}
}
五. 左单旋
如下是红黑树中左单旋常见的三个情景:
过程:
- 让parent的_right去连接subR的左子树subRL,如果subRL不为NULL,让subRL的_parent去连接parent.
- 然后parent的_parent连接subR,subR的_left连接parent.
- 最后,subR的parent去连接pparent,如果pparent存在,需要判断subR连接在pparent中的哪颗子树,如果parent位于pparent中的左子树,则parent的_left连接subR,如果parent位于pparent的右子树,则pparent的_right连接subR。
- 如果pparent为NULL,则说明parent是根节点,旋转后,则subR变为根节点。
总结:
总共是6次连接,subRL与parent之间互相连接,subR与parent之间互相连接,subR与pparent之间互相连接。
代码:
void RotateLeft(Node* parent)
{
Node* subR = parent->_right;
Node* pparent = parent->_parent;
Node* subRL = subR->_left;
//subRL与parent之间的互相连接
parent->_right = subRL;
if (subRL)
{
subRL->_parent = parent;
}
//parent与subR之间的互相连接
parent->_parent = subR;
subR->_left = parent;
//subR与pparent之间互相连接
subR->_parent = pparent;
if (pparent)
{
if (pparent->_left == parent)
{
pparent->_left = subR;
}
else if (pparent->_right == parent)
{
pparent->_right = subR;
}
}
else//pparent为NULL
{
_root = subR;
}
}