前言
各位读者朋友们大家好!上期我们讲了二叉搜索树之一——AVL树,这一期我们继续讲解另一种二叉搜索树——红黑树。
目录
- 前言
- 一. 红黑树的概念
- 1.1 红黑树的规则
- 1.2 红黑树如何确保最长路径不超过最短路径的两倍
- 1.3 红黑树的效率
- 二. 红黑树的实现
- 2.1 红黑树的结构
- 2.2 红黑树的插入
- 2.2.1 红黑树插入的大致过程
- 2.2.2 情况一:只变色
- 2.2.3 情况二:单旋+变色
- 2.2.4 情况三:双旋+变色
- 2.2.5 插入的代码实现
- 2.3 红黑树的验证
- 三. 红黑树和AVL树对比
- 结语
一. 红黑树的概念
红黑树是⼀棵二叉搜索树,他的每个节点增加一个存储位来表示节点的颜色,可以是红色或者黑色。通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是接近平衡的。
1.1 红黑树的规则
- 每个节点不是黑色就是红色
- 根节点一定是黑色
- 如果一个节点是红色,则她的两个孩子一定是黑色,也就是说,任意一条连续的路径不允许连续出现两个红色节点
- 对于任意一个节点,从该节点到其所有NULL节点的所有简单路径上的黑色节点个数相同
如上就是一棵红黑树,根节点是黑色的,任意一条路径上没有连续的两个红色节点,从根节点到NULL节点一共有9条路径,每一条路径上都有相等个数的黑色节点。
1.2 红黑树如何确保最长路径不超过最短路径的两倍
综合红黑树的规则来看,理论上全黑最短路径和一黑一红节点的最长路径并不是在每棵红黑树都存在的。假设任意一条从根到NULL节点的路径长度为x,那么bh(black height) <= h <= 2*bh。
1.3 红黑树的效率
假设N是红黑树节点的个数,h为最短路径的长度,那么2h-1 <= N <= 22h-1,由此推出,h接近于logN,也就是意味着红黑树的增删改查的最坏情况就是走最长路径2 * logN,时间复杂度还是O(logN)。
总体来说,红黑树比AVL树要抽象一点,AVL树通过高度差更加直观的控制了平衡,红黑树通过4条规则的颜色约束,间接的实现了近似平衡,他们的效率都是logN级别的,但是相对而言,插入相同个数的节点,红黑树旋转的次数更少,因此它的平衡的控制没有AVL树那么严格。
二. 红黑树的实现
2.1 红黑树的结构
// 枚举值表示颜色
enum Colour
{RED,BLACK
};
// 这里我们默认按key/value结构实现
template<class K, class V>
struct RBTreeNode
{// 这里更新控制平衡也要加入parent指针 pair<K, V> _kv;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;Colour _col;RBTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr){}
};
template<class K, class V>
class RBTree
{typedef RBTreeNode<K, V> Node;
public:private:Node* _root = nullptr;
};
2.2 红黑树的插入
2.2.1 红黑树插入的大致过程
- 插入一个值按照二叉搜索树的规则进行插入,插入后我们只需检查是否符合红黑树的4条规则。
- 如果是空树插入,则插入的新节点是黑节点;如果是非空树插入,新增节点一定是红色节点,因为非空树插入,新增黑色节点就违反了规则4,规则4是很难维护的。
- 非空树插入后,新增节点一定是红色节点,如果父亲节点是黑色节点,没有违反任何规则,插入结束。
- 非空树插入后,新增节点一定是红色节点,如果父亲节点是红色节点,就违反了规则3
因此根据uncle的颜色和是否存在分以下情况处理:
2.2.2 情况一:只变色
p为红色节点,g为黑色节点,叔叔节点存在且为红色节点
在这种情况下,无论c是p的左还是右,p是g的左还是右,都是采取变色的处理方式
更新完一次后,需要继续向上更新处理。
g是红色,如果g的父亲还是红色,那么就还需要继续处理;如果g的父亲是黑色,则处理结束;如果g就是整棵树的根,再把g变回黑色。
我们切到抽象图来看一下:
代码实现:
bool Insert(const pair<K, V>& kv)
{if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}Node* parent = nullptr;Node* cur = _root;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{return 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)// 确保parent存在,如果cur是根节点,parent就是空指针{Node* grandfather = parent->_parent;if (parent == grandfather->_left){// g // p uNode* uncle = grandfather->_right;if (uncle && uncle->_col == RED)// 叔叔存在且为红{parent->_col = uncle->_col = BLACK;grandfather->_col = RED;// 继续向上更新cur = grandfather;parent = cur->_parent;}}else{// g // u pNode* uncle = grandfather->_left;if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfather->_col = RED;// 继续向上更新cur = grandfather;parent = cur->_parent;}}}_root->_col = BLACK;// 保证根节点始终为黑色return true;
}
2.2.3 情况二:单旋+变色
c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c一定是新增节点(如果c是变色变上来的,那么路径上至少有两个黑节点(g和变色的节点),但是u不存在,路径上黑色节点个数不相等,所以c一定是新增节点);u存在且为黑,则c一定不是新增的,c之前是黑色的,是在c的子树中插入
- 右单旋(c是p的左,p是g的左)
- 左单旋(c是p的右,p是g的右)
以上两种情况,是变色之后继续向上调整产生的,旋转变色之后就不需要继续向上调整,因为新的根节点是黑色,不可能出现连续两个红色节点了。
代码实现:
if (cur == parent->_left)//右单旋
{RotateR(grandfather);parent->_col =BLACK;grandfather->_col = RED;
}
if (cur == parent->_right)//左单旋
{RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;
}
2.2.4 情况三:双旋+变色
c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c一定是新增节点,u存在且为黑,则c一定不是新增,c之前是黑色的,是在c的子树中插入,符合情况一,变色将c从黑色变为红色,更新上来的。
- 左右双旋
从结果来看,整个旋转和变色的过程是p为旋转点左单旋,g为旋转点右单旋,c变成子树的根,a变成p的右子树,b变成g的左子树,p和g分别变成c的左右子树,c变黑,g变红。
以上这种情况是左右双旋,即c是p的右,p是g的左,另一种情况是c是p的左,p是g的右,要先以p为旋转点右单旋,再以g为旋转左单旋。 - 右左双旋
这样处理完之后不需要继续向上更新,因为根变成黑色节点了。
2.2.5 插入的代码实现
bool Insert(const pair<K, V>& kv)
{if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}Node* parent = nullptr;Node* cur = _root;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{return 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)// 确保parent存在,如果cur是根节点,parent就是空指针{Node* grandfather = parent->_parent;if (parent == grandfather->_left){// g // p uNode* uncle = grandfather->_right;if (uncle && uncle->_col == RED)// 叔叔存在且为红{parent->_col = uncle->_col = BLACK;grandfather->_col = RED;// 继续向上更新cur = grandfather;parent = cur->_parent;}else// 叔叔不存在或者叔叔为黑{// g // p u// cif (cur == parent->_left)//右单旋{RotateR(grandfather);parent->_col =BLACK;grandfather->_col = RED;}else{// g // p u// cRotateL(parent);RotateR(grandfather);// 变色cur->_col = BLACK;grandfather->_col = RED;}break;}}else{// g // u pNode* uncle = grandfather->_left;if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfather->_col = RED;// 继续向上更新cur = grandfather;parent = cur->_parent;}else{// g // u p// cif (cur == parent->_right)//左单旋{RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{// g // u p// cRotateR(parent);RotateL(grandfather);// 变色cur->_col = BLACK;grandfather->_col = RED;}break;}}}_root->_col = BLACK;// 保证根节点始终为黑色return true;
}void RotateR(Node* parent)
{// subL当根节点,parent当subL的右子树,subLR当parent的右子树,注意所有的parent的处理Node* subL = parent->_left;Node* subLR = subL->_right;subL->_right = parent;parent->_left = subLR;// 从下往上链接父节点if (subLR)// subL可能为空,为空就不需要处理父亲节点subLR->_parent = parent;Node* pParent = parent->_parent;// 父节点的父亲,便于后续链接parent->_parent = subL;if (parent == _root)// 如果调整的是整棵树的根{_root = subL;subL->_parent = nullptr;}else// 局部树,链接祖先{if (parent == pParent->_left)pParent->_left = subL;elsepParent->_right = subL;subL->_parent = pParent;}
}//左单旋
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;subR->_left = parent;// 从下往上链接父节点if (subRL)subRL->_parent = parent;Node* pParent = parent->_parent;parent->_parent = subR;if (parent == _root)// 调整的是整棵树的根{_root = subR;subR->_parent = nullptr;}else{if (parent == pParent->_left)pParent->_left = subR;elsepParent->_right = subR;subR->_parent = pParent;}
}
2.3 红黑树的验证
验证红黑树前两条规则很容易处理,3、4条规则处理起来有些麻烦
对于第三条规则,如果通过前序遍历找孩子验证起来会有些麻烦,要区分左孩子和右孩子,我们可以通过红色节点,看其父亲节点是不是红色节点来验证;对于第四条规则,我们可以通过一个形参记录从根节点到该节点的黑色节点的数量,通过递归处理,递归传形参,在递归建立的新的栈帧里,传递的形参不会被改变,再通过一个标准值,来比对每条路径黑色节点的个数是否相等。
每层栈帧中存储从根节点到当前节点的路径上黑色节点的个数
bool IsBalance()
{if (_root == nullptr)return true;if (_root->_col == RED)return false;int refNum = 0;// 标准值取最左路径黑色节点的个数Node* cur = _root;while (cur){if (cur->_col == BLACK)++refNum;cur = cur->_left;}return Check(_root, 0, refNum);
}bool Check(Node* root, int BLACKNum, const int refNum)
{if (root == nullptr){if(BLACKNum != refNum){cout << "黑色节点个数不相等" << endl;return false;}return true;}if (root->_col == RED && root->_parent->_col == RED)// 调用Check函数之前已经确保了根节点不是红色节点{cout << "出现两个连续的红色节点" << endl;return false;}if (root->_col == BLACK)++BLACKNum;return Check(root->_left, BLACKNum, refNum) && Check(root->_right, BLACKNum, refNum);
}
三. 红黑树和AVL树对比
AVL树是一颗绝对平衡的二叉搜索树,红黑树对比AVL树就没有那么平衡了。
在Debug模式下,二者插入同样的随机数,时间相差不大,由于AVL树绝对平衡,因此AVL树的高度要低于红黑树的高度,但是红黑树在插入过程中旋转的次数要少于AVL树,二者在Debug模式下搜索的效率也相差不大。
换到Release模式,搜索偶的效率得到了提升。
红黑树代码实现
结语
以上我们就讲完了红黑树的内容,感谢大家的阅读,希望对大家有所帮助,欢迎大家批评指正!