上一篇我们已经了解了关于AVL树的基本情况和实现方式了,本次我们继续来了解平衡树的另一种树——红黑树。
首先,我们知道AVL树是通过控制平衡因子来达到它们左右子树的高度差的绝对值不超过1的方式使树接近平衡。而红黑树也是平衡树的一种,那么有人就会疑惑了,既然有了AVL树理论后,为什么还要有红黑树的呢?
答:因为AVL树是一颗绝对的平衡树,它要求每个节点左右子树的高度差的绝对值不超过1,只有这样的树才能到达log2N的效率,但是如果对AVL树进行一些关于结构性的修改,其效率会很低效。即如果对它进行插入的时候,由于要进行旋转的次数多,删除的时候,还有可能一直旋转到根。所以那样的效率就非常低了,所以红黑树就诞生于这种需求能改变结构且效率还不错的情况了。
红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的
定义与性质
- 节点颜色:红黑树的每个节点都有颜色,要么是红色,要么是黑色。- 根节点:根节点是黑色的。
- 叶节点:红黑树的叶节点是NIL节点,它们都是黑色的。
- 红黑规则:每个红色节点的两个子节点都是黑色(即任何路径没有连续的红色节点),从任一节点到其每个叶节点的所有路径都包含相同数目的黑色节点。
解释NIL:(简单点:红黑树中,看成叶子节点,并且要求NIL叶节点都是黑色的)
在红黑树中,NIL节点是一种特殊的节点,也被称为哨兵节点或空节点,具有以下特点:
- 表示空指针:NIL节点用于表示二叉树中的空指针。在实际的红黑树实现中,它可以是一个单独的节点对象,其所有指针都指向自身或者是一个特定的空值。
- 颜色为黑色:NIL节点被定义为黑色,这是红黑树性质的要求,有助于保证红黑树的平衡和相关操作的正确性。
- 辅助树的操作:NIL节点使得红黑树的一些操作,如插入、删除和遍历等,能够以统一的方式进行处理,简化了算法的实现和逻辑。例如,在遍历红黑树时,遇到NIL节点就表示已经到达了树的叶子节点,可以停止遍历。
判断下面是不是红黑树?
![]()
答案:不是,因为8的左子树那条路径只有一个黑色节点,而其他的路径都有两个黑色节点。所以并不符合红黑树的规则。
比较AVL与红黑树的性能:
因此,它们的性能都是在同一量级的,但是AVL树严格控制平衡是要付出代价的,即插入删除需要大量的旋转
本文只做关于插入部分:
颜色部分
我们使用枚举来设置颜色
enum Color
{
BLACK,
RED
};
构建节点成员变量:
这里跟AVL树很相似,只不过这里是用颜色,那里是用平衡因子来控制而已。比较常规。
值得一提的是:下面的初始化列表中,为什么把颜色默认初始化为RED呢?
这是因为由上面红黑树的定义知道,各个路径的黑节点个数是要一样的,如果你默认颜色以黑色的话,那么当你插入节点后,各个路径的节点黑色个数必定会不相等了,这时候是一个要更新树节点的颜色的,这回造成一定的效率问题,反而如果你以红色为默认颜色时,有时候插入,不会影响它红黑树的规则,这会减少了一部分的时间消耗,提高了效率,因此我们默认把颜色弄成红色。
template<class K,class V> struct RBTreeNode { RBTreeNode<K, V>* _left; RBTreeNode<K, V>* _right; RBTreeNode<K, V>* _parent; pair<K, V> _kv; //颜色的变量 Color _col; //初始化列表进行初始化 RBTreeNode(const pair<K,V>& kv) :_left(nullptr) ,_right(nullptr) ,_parent(nullptr) ,_kv(kv) ,_col(RED) { } };
构建树的结构变量
template<class K,class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
private:
//这里我们弄成空,即代表一开始是当作空树
Node* _root = nullptr;
};
插入部分:
当插入的位置是下面的情况时,我们是不用管的,因为它并不影响我们的红黑树规则(这也是上面我们是为什么要初始化颜色为红色的原因)
而当我们插入位置类似下面的情况:这会影响我们的红黑树的规则,所以需要更新颜色,必要时还要进行旋转操作。(具体的看上面讲解的具体情况)
不同插入的情况:
1.uncle存在且uncle为红
uncle不存在:
uncle存在且uncle为黑:
因此:
红黑树的插入看uncle
1.uncle存在且为红,变色+继续向上更新
2.uncle不存在,且uncle为黑,旋转+变色
1.由上面的分析,我们还要定义grandfather,uncle ,cur等变量
2.由上面的图分析知道,当我们的parent存在,且parent的颜色也是红色的话,才需要进行颜色的更新,必要时进行旋转。
bool insert(const pair<K, V>& kv) { //一开始树就为空,就直接new创建节点 if (_root == nullptr) { _root = new Node(kv); //树的根节点要为黑,所以节点颜色要弄成黑 _root->_col = BLACK; return true; } Node* cur = _root; Node* parent = nullptr; //找要插入的位置,这里跟搜索二叉树思路一样的 while (cur) { if (cur->_kv.first < kv.first) { parent = cur; cur = cur->_right; } else if (cur->_kv.first > kv.first) { parent = cur; cur = cur->_left; } else { assert(false); } } //找到了,开始插入节点 cur = new Node(kv); cur->_col = RED; if (parent->_kv.first < kv.first) { parent->_right=cur; } else { parent->_left=cur; } cur->_parent = parent; //父亲节点为红色 while (parent && parent->_col == RED) { Node* grandfather = parent->_parent; if (parent == grandfather->_left) { Node* uncle = grandfather->_right; if (uncle && uncle->_col == RED) { //uncle存在,且为红色,将parent,uncle颜色变黑,grandfather变红 parent->_col = BLACK; uncle->_col = BLACK; grandfather->_col = RED; //继续向上更新 cur = grandfather; parent = cur->_parent; } else//不存在 或者uncle为黑 旋转+变色 { if (cur == parent->_left) { // g // p //c RotateR(grandfather); parent->_col = BLACK; grandfather->_col = RED; } else //cur==parnet->right { // g // p // c RotateL(parent); RotateR(grandfather); cur->_col = BLACK; //parent->_col = BLACK; grandfather->_col = RED; } break; } } else //(parent == grandfather->_right) { Node* uncle = grandfather->_left; //Node* uncle = grandfather->_right; if (uncle && uncle->_col == RED) { parent->_col = uncle->_col = BLACK; grandfather->_col = RED; //继续向上更新 cur = grandfather; parent = cur->_parent; } else //(uncle不存在时)旋转+变色 { // g // p // c if (cur == parent->_right) { RotateL(grandfather); parent->_col = BLACK; grandfather->_col = RED; } // g // p // c else { RotateR(parent); RotateL(grandfather); cur->_col = BLACK; //parent->_col = BLACK; grandfather->_col = RED; } break; } } } //更新完后,可能会将根节点变成红色,所以要更新变成黑色 _root->_col = BLACK; return true; }
旋转部分:
这里部分跟AVL和搜索二叉树的左旋转,右旋转的差不多,而且在那里已经讲解过了,这里就不再多重复了。有需要的可以先去了解了解噢
左旋转
// f f
// a b
// b ------------ a c
// d c d
//
void RotateL(Node*parent)
{
Node* cur = parent->_right;
Node* curleft = cur->_left;
parent->_right = curleft;
if (curleft)
{
curleft->_parent = parent;
}
cur->_left = parent;
Node* ppnode = parent->_parent;
parent->_parent = cur;
if(parent==_root)
//***if (cur == _root)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (parent == ppnode->_right)
{
ppnode->_right = cur;
cur->_parent = ppnode;
}
else
{
ppnode->_left = cur;
cur->_parent = ppnode;
}
}
}
右旋转
// a b
// b c d a
// d e -------> f e c
// f
//
void RotateR(Node* parent)
{
Node* cur = parent->_left;
Node* curright = cur->_right;
parent->_left = curright;
if (curright)
{
curright->_parent = parent;
}
cur->_right = parent;
Node* ppnode = parent->_parent;
parent->_parent = cur;
if (cur == _root)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (parent == ppnode->_left)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
}
同样,在AVL树中,我们有如何检验它是否正确,所以这里也是一样(主要是学习它如何进行程序验证的思路~)
验证其是否正确:
颜色检查:
思路:利用它的各个路径的黑色节点的个数都相等的特性,计算出它黑色节点的个数是否与基准值相等,如果相等说明颜色没有问题,反之就有。
2.如果出现了连续两个红色节点的话, 也是说明错误了,违反了它的规则了。
//benchmarck基准 bool CheckColor(Node*root,int blacknum,int benchmarck) { if (root == nullptr) { if (blacknum != benchmarck) { return false; } return true; } if (root->_col == BLACK) { blacknum++; } if (root->_col == RED && root->_parent && root->_parent->_col == RED) { cout << root->_kv.first << "出现连续红色节点" << endl; return false; } return CheckColor(root->_left, blacknum, benchmarck) && CheckColor(root->_right, blacknum, benchmarck); }
判断是否平衡
以这里以最左子树为的黑色节点个数作为基准值(随你用哪一条路径)。
bool IsBalance() { return IsBalance(_root); } bool IsBalance(Node*root) { if (root == nullptr) return true; if (root->_col == RED) return false; //计算基准值 int benchmarck = 0; Node* cur = root->_left; while (cur) { if (cur->_col == BLACK) { benchmarck++; } cur = cur->_left; } return CheckColor(root,0,benchmarck); }
求高度:
返回最高的一边树。
int Height() { return Height(_root); } int Height(Node*root) { if (root == nullptr) return 0; int leftheight = Height(root->_left); int rightheight = Height(root->_right); return leftheight > rightheight ? leftheight + 1 : rightheight + 1; }
好了,本次,关于红黑树的简单了解的分析就到处结束了,希望我们一起进步!
最后,到了本次鸡汤环节: