数据结构之红黑树
我之前的博客已经介绍过了二叉树和AVL树的基本概念和简单实现,具体参考数据结构-树(C语言实现篇)和数据结构之AVL树(C++实现)。
1. 红黑树
1.1 红黑树的概念
红黑树也是一种二叉搜索树,在此基础上每个结点增加一个存储位表示结点的颜色,可以是红色或者黑色,通过对任何一条从根节点到叶子结点的路径上每个结点颜色的限制,红黑树确保没有一条路径会比其他路径大一倍,因此,是接近平衡的一颗二叉搜索树。
1.2 红黑树的性质
- 每个结点的颜色不是红色就是黑色。
- 根节点是黑色的。
- 如果一个节点是红色的,那么它的两个孩子结点是黑色的。
- 对于每个结点,从该结点到其所有后代的叶子结点的路径上,均包含相同数目的黑色结点。
- 每个叶子结点都是黑色的**(指的是空节点)**。
2. 红黑树的实现
2.1 红黑树结点的定义
// 结点的颜色
enum Colour {
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode
{
RBTreeNode<K, V>* _left; // 左孩子
RBTreeNode<K, V>* _right; // 右孩子
RBTreeNode<K, V>* _parent; // 父节点
pair<K, V> _kv; // 存储对
Colour _col; // 存储颜色
RBTreeNode(const pair<K, V>& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _kv(kv)
{}
};
2.2 红黑树的插入操作
红黑树是在二叉搜索树的基础上加上了平衡限制条件,故分为两步:
-
按照二叉搜索树的规则插入新节点。
typedef RBTreeNode<K, V> Node; 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); if (parent->_kv.first < kv.first) { parent->_right = cur; } else { parent->_left = cur; } cur->_parent = parent; }
-
检测新结点插入之后,红黑树的性质是否被破坏。
因为新插入的结点默认是红色的,因此,如果双亲结点是黑色,则没有违反红黑树任何性质。如果插入结点的双亲结点是红色的,就违背了
红色结点的两个孩子结点是黑色的
的性质。针对不同的情况进行分析:- 情况一:cur为红色,parent为红色,grandfather为黑色,uncle为红色。
设置双亲结点和双亲的兄弟结点为黑色。
如果g结点为根节点,那么设置为黑色。因为根节点必须是黑色。
如果g结点不为根节点,那么设置为红色,然后继续向上调整。
- 情况二:cur为红色,parent为红色,grandfather为黑色,uncle不存在/存在且为黑色。
说明:(u结点分两种情况)
如果u结点不存在,则cur一定是新插入的结点,因为cur和p都为红色,故冲突发生在这里,一定是有一个结点不满足,才会导致性质失效。
如果u结点存在,则其一定是黑色的,那么cur结点原来的颜色也是黑色的,因为如果cur原来不是黑色的,就不满足从根节点到叶子结点的黑色节点数量相同。现在看到是黑色的原因在于cur的子树在调整过程中将cur结点的颜色从黑色改为红色。
p结点为g结点的左孩子,cur结点为p结点的左孩子,进行右单旋。
p结点为g结点的右孩子,cur结点为p结点的右孩子,进行左单旋。
p结点变成黑色,g结点变成红色。
- 情况三:cur为红色,parent为红色,grandfather为黑色,uncle不存在/存在且为黑色。
p结点为g结点的左孩子,cur结点为p的右孩子,则对p做左单旋。
p结点为g结点的右孩子,cur结点为p的左孩子,则对p做右单旋。
最终变成了情况二,按照情况二接着处理即可。
-
代码实现:
// parent为红色 while (parent && parent->_col == RED) { Node* grandfater = parent->_parent; assert(grandfater); assert(grandfater->_col == BLACK); // 如果爷爷的颜色不为黑色,那么前面的操作有误 // 关键看叔叔 // 若p为g左子树 if (parent == grandfater->_left) { Node* uncle = grandfater->_right; // 情况一 : uncle存在且为红,变色+继续往上处理 // 情况一只用变色,不需要旋转 if (uncle && uncle->_col == RED) { parent->_col = uncle->_col = BLACK; grandfater->_col = RED; // 继续往上处理 cur = grandfater; parent = cur->_parent; }// 情况二+三:uncle不存在 + 存在且为黑 else { // 情况二:右单旋+变色 // g // p u // c if (cur == parent->_left) { RotateR(grandfater); // 采用AVL树的旋转策略 parent->_col = BLACK; grandfater->_col = RED; } else { // 情况三:左右单旋+变色 // g // p u // c RotateL(parent); // 这一步结束后变成了情况二 RotateR(grandfater); cur->_col = BLACK; grandfater->_col = RED; } // 不需要在往上调整 break; } } else // (parent == grandfater->_right) { Node* uncle = grandfater->_left; // 情况一 if (uncle && uncle->_col == RED) { parent->_col = uncle->_col = BLACK; grandfater->_col = RED; // 继续往上处理 cur = grandfater; parent = cur->_parent; } else { // 情况二:左单旋+变色 // g // u p // c if (cur == parent->_right) { RotateL(grandfater); parent->_col = BLACK; grandfater->_col = RED; } else { // 情况三:右左单旋+变色 // g // u p // c RotateR(parent); RotateL(grandfater); cur->_col = BLACK; grandfater->_col = RED; } break; } } } // 记得最后需要将根节点设置为黑色 _root->_col = BLACK;
2.3 红黑树的验证
红黑树的验证分为两步:
-
检测其是否满足二叉搜索树(中序序列是否是有序序列)。
void InOrder() { _InOrder(_root); cout << endl; } void _InOrder(Node* root) { if (root == nullptr) { return; } _InOrder(root->_left); cout << root->_kv.first << ":" << root->_kv.second << endl; _InOrder(root->_right); }
-
检测是否满足红黑树的性质。
bool IsBalance() { /* 性质: 1. 根节点是黑色的 2. 如果一个节点是红色的,则它的两个孩子结点是黑色的 3. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 4. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点) */ if (_root == nullptr) return true; if (_root->_col == RED) { cout << "根节点不是黑色" << endl; return false; } // 黑色节点数量基准值 int benchmark = 0; return PrevCheck(_root, 0, benchmark); } /* 思路: 判断每条路径上的黑色节点个数是否相同。 blackNum为当前路径结点的个数 benchmark第一次为0,第二次为第一次获取路径结点的个数 如果benchmark不等于blackNum,表示当前路径的黑色节点个数与其他路径不相同,则不满足红黑树的条件。 */ bool PrevCheck(Node *root, int blackNum, int &benchmark) { if (root == nullptr) { if (benchmark == 0) { benchmark = blackNum; return true; } if (blackNum != benchmark) { cout << "黑色节点数量不相等" << endl; return false; } else { return true; } } // 如果为黑色,那么黑色节点数量++ if (root->_col == BLACK) ++blackNum; // 检查当前结点与其双亲结点是否都为红色 if (root->_col == RED && root->_parent->_col == RED) { cout << "出现连续的红色结点" << endl; return false; } return PrevCheck(root->_left, blackNum, benchmark) && PrevCheck(root->_right, blackNum, benchmark); }
2.4 红黑树的删除
红黑树的删除这里不做讨论,有兴趣可以参考:《算法导论》或者《STL源码剖析》
https://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html
2.5 红黑树与AVL树的比较
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log_2(n))
,红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。