【C++高阶数据结构】红黑树
前言:
前面我们已经理解并实现了AVL树,不难发现:AVL树对其自身结构有非常严格的要求,即任意节点的左右子树高度差不能超过1,所以,又有人提出了红黑树这样的数据结构,但AVL树与红黑树都遵循二叉搜索树的规则。
🚀直通车:《我的数据结构专栏》
一、什么是红黑树?
1.1、红黑树概念
红黑树是一棵二叉搜索树,他的每个结点增加一个存储位来表示结点的颜色,可以是红色或者黑色。 通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路 径长出2倍,因而是接近平衡的。
1.2、红黑树规则
• 根结点为黑色;
• 每个结点不是黑色就是红色;
• 如果结点为红色,那么该节点的两个孩子节点为黑色,即任意一条路径上没有连续的红色节点;
• 对于任意一个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的黑色结点。
思考:红黑树如何确保最长路径不超过最短路径的2倍的?
答:从根结点开始的一条路径上只有n个黑色结点,由红黑树规则,两条路径上黑色结点数相同,且红色结点不连续,则当另一条路径上黑色结点与红色结点相间分布时,有最长长度为2n,这就保证了最长路径始终不超过最短路径的两倍。
1.3、红黑树的效率
假设N是红黑树树中结点数量,h最短路径的长度,那么2^h − 1 <= N < 2^(2∗h) − 1 , 由此推出
h ≈ logN ,也就是意味着红黑树增删查改最坏也就是走最长路径 2 ∗ logN,那么时间复杂度还是 O(logN)
二、红黑树的实现
说明:我们以实现一个键值对(key_value)类型的红黑树,且数据不支持冗余。
2.1 红黑树节点结构定义
对于结点,我们需要一个pair来存储键值对;left指针指向左孩子结点;right指针指向右孩子结点;color变量存储结点颜色;后面插入结点时,如果需要调整平衡,则要频繁地访问父亲结点,所以还需要一个parent指针指向父亲结点(与AVL数相同)。
由于结点颜色只有黑或者红,而enum(枚举类型)可以用于定义固定集合常量,所以可以将结点颜色存储在一个枚举类型中。
enum Color {RED,BLACK };
节点结构:
template<class K,class V> struct RBTreeNode {pair<K, V> _kv;RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;Color _col;RBTreeNode(const pair<K,V>& kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr){} };
2.2、红黑树的结构
template<class K,class V>
class RBTree
{typedef RBTreeNode<K, V> Node;
public:// ...
private:Node* _root = nullptr; // 根结点
};
2.3、插入
当为空树时,插入节点作为根结点且颜色为黑;不为空时,插入结点就要满足红黑树的规则,如果插入黑色结点,则会打破规则四(对于任意一个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的黑色结点),所以,新插入的结点颜色一定为红色。
而对于新插入节点的父亲结点(parent)和父亲结点的父亲节点(grandfather)颜色,进一步分析,若parent为黑,则直接插入;若parent为红,则grandfather一定黑,插入新节点(红),违反规则,需要处理。此时,就需要根据父亲结点的兄弟结点的状态来进一步分情况讨论:
说明:下图中假设我们把新增结点标识为c (cur),c的父亲标识为p(parent),p的父亲标识为 g(grandfather),p的兄弟标识为u(uncle)。
当新插入结点的parent为红,即出现连续的红色结点,需要处理,大概分为下面两种情况:
情况一:u结点存在且为红
• u 为右孩子:
• u 为左孩子
情况一又根据 u 结点是左孩子还是右孩子分为两种情况,但是,对于这两种情况处理方式相同,即将 g 变为红,u 和 p 变为黑。但是,需要注意一点:g 变为红色后,也有可能 g 的父亲结点为红色,所以需要继续向上处理,即 c 指向 g ,p 和 g 同时更新,直到所有结点满足红黑树规则。
情况二:u结点存在为黑或不存在
此时 u 结点对于红黑树调整没有影响,但是需要考虑 p 和 c 结点的位置。
• p 为右孩子且 c 为右孩子(左单旋+变色)
• p 为右孩子且 c 为左孩子(双旋+变色)
• p 为左孩子且c 为左孩子(右单旋+变色)
• p 为左孩子且c 为右孩子(双旋+变色)
代码实现:
对于旋转调整平衡我们在前面的AVL树中已经做了详细地介绍,如果有什么问题,大家可以移步:【数据结构】AVL树:从原理到旋转平衡艺术
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 (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else if (kv.first > cur->_kv.first){parent = cur;cur = cur->_right;}else{return false;}}// -------------找到后开始插入----------------------cur = new Node(kv);cur->_col = RED; // -------------------新插入的节点一定为红,否则会改变黑节点的数if (kv.first < parent->_kv.first){parent->_left = cur;}else{parent->_right = cur;}// -------------------链接父亲节点---------------------cur->_parent = parent;// -------------------当出现连续的红色结点,开始调整------------------------while (parent && parent->_col == RED){Node* grandfather = parent->_parent;// -------------处理所有 p 为左孩子的情况---------------------------if (parent == grandfather->_left){// g// p uNode* uncle = grandfather->_right;if (uncle && uncle->_col == RED) // ----------叔叔存在且为红(情况一){// -----------------变色--------------------------grandfather->_col = RED;parent->_col = BLACK;uncle->_col = BLACK;// -----------------继续向上处理--------------------cur = grandfather;parent = cur->_parent;}else // ------------------------叔叔不存在或者为黑(情况二){if (cur == parent->_left){// g// p u// cRotateR(grandfather); // ---------右旋// -----------变色--------------------parent->_col = BLACK;grandfather->_col = RED;}else{// g// p u// cRotateL(parent); // --------------左旋RotateR(grandfather); // ---------右旋// -----------变色--------------------cur->_col = BLACK;grandfather->_col = RED;}break;}}else // ---------------------------处理所有 p 为右孩子的情况{// g// u pNode* uncle = grandfather->_left;if (uncle && uncle->_col == RED) // ----------叔叔存在且为红(情况一){// 变色grandfather->_col = RED;parent->_col = BLACK;uncle->_col = BLACK;// 继续向上调整cur = grandfather;parent = cur->_parent;}else // -----------------------------------叔叔不存在或者为黑(情况二){if (cur == parent->_right) {// g// u p// cRotateL(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;
}
左旋:
相比于AVL树,旋转代码只需要去掉修改平衡因子的代码即可
void RotateL(Node* parent)
{Node* SubR = parent->_right;Node* SubRL = SubR->_left;Node* pParent = parent->_parent;// --------------连接parent与SubRL----------------parent->_right = SubRL;if (SubRL)SubRL->_parent = parent;// --------------连接parent与SubR----------------SubR->_left = parent;parent->_parent = SubR;// -----------------------连接SubR与pParentif (pParent == nullptr) // -------------------判断parent是否为根结点{_root = SubR;SubR->_parent = nullptr;}else{if (parent == pParent->_left){pParent->_left = SubR;}else{pParent->_right = SubR;}SubR->_parent = pParent;}
}
右旋:
void RotateR(Node* parent)
{Node* SubL = parent->_left;Node* SubLR = SubL->_right;Node* pParent = parent->_parent;// ----------------------连接SubLR与parent-----------------------parent->_left = SubLR;if (SubLR)SubLR->_parent = parent;// ----------------------连接SubL与parent-----------------------SubL->_right = parent;parent->_parent = SubL;// -----------------------连接SubL与pParentif (parent == _root) // -------------------判断parent是否为根结点{_root = SubL;SubL->_parent = nullptr;}else{if (pParent->_left == parent){pParent->_left = SubL;}else{pParent->_right = SubL;}SubL->_parent = pParent;}
}
2.4、查找
遍历红黑树即可
Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_kv.first < key){cur = cur->_right;}else if (cur->_kv.first > key){cur = cur->_left;}else // --------------------找到了{return cur;}}return nullptr;}
2.5、判断平衡(验证)
bool IsBalanceTree()
{if (_root == nullptr) //--------------空树也是红黑树{return true;}if (_root->_col == RED) // -------------根结点为红色{return false;}int hb = CountBlackNode(); // --------------获取基准值return _IsbalanceTree(_root,0, hb); // ------------调用子函数
}// ---------------按左边或者最右边一条路径来统计黑色节点的数量,作为标准----------------------
int CountBlackNode()
{Node* cur = _root;int count = 0;while (cur){if (cur->_col == BLACK){count++;}cur = cur->_left;}return count;
}// 将每一条路径上的黑色节点数作为一个参数,将上面得到的黑色节点数(基准)作为参数用来和blacknum做对比
bool _IsbalanceTree(Node* root,int balackNum,int hb)
{if (root == nullptr){// ------------------前序遍历走到空时,意味着一条路径走完了---------------------if (balackNum != hb){cout << "存在黑色结点的数量不相等的路径" << endl;return false;}return true;}// 检查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲就方便多了if (root->_col == RED && root->_parent->_col == RED){cout << root->_kv.first << "存在连续的红色结点" << endl;return false;}if (root->_col == BLACK){balackNum++;}return _IsbalanceTree(root->_left, balackNum, hb) && _IsbalanceTree(root->_right, balackNum, hb);
}