赞
踩
红黑树是一种近似平衡的二叉搜索树,它有以下特点:
NIL
节点,在使用时我们也可以忽略它的存在。红黑树这些规则有什么用呢?
红黑树的最优情况:全为黑色节点或每条路径都是一黑一红
红黑树的最差情况:左子树全黑,右子树一黑一红
红黑树是一个近似平衡的搜索二叉树,它确保了没有一条路径会比其他路径长出2倍,解决了普通二叉搜索树因不平衡导致查找效率低的问题。
AVL树也是一颗近似平衡的二叉搜索树,它们有什么区别?
- AVL树中左子树与右子树的高度差不能超过1,红黑树中最长路径不会超过最短路径的2倍。所以从平衡性来讲,AVL树比红黑树更加接近平衡,AVL树的查找效率比红黑树更高
- 当插入节点时,AVL树中平衡因子超过2就要进行旋转,而红黑树中出现连续红色节点时才会旋转。这是红黑树的旋转次数比AVL树少,因此红黑树的插入效率比AVL树更高。同理,删除效率也比AVL树更高
虽然选择二叉搜索树作为数据结构去使用是因为它高效率的查找效率,但是在我们经常使用的一些容器中,插入和删除也是我们频繁使用的接口,因此在实际应用中,红黑树的应用比AVL树更广泛。但是对于一些特定场合,使用AVL树也是更不错的选择。
enum
枚举类型来定义节点的红色和黑色namespace Name { // 颜色定义 enum Color { RED, BLACK }; // 节点声明 template <class K> struct RBTreeNode { K& _key; // 键 RBTreeNode<K> *_left; RBTreeNode<K> *_right; RBTreeNode<K> *_parent; Color _col; // 颜色 // 构造函数 RBTreeNode(const K& key) : _key(key) , _left(nullptr) , _right(nullptr) , _parent(nullptr) , _col(RED) { ; } }; // 红黑树的声明 template <class K> class RBTree { typedef RBTreeNode<K> Node; // 将节点类型重命名为Node public: bool Insert(const K& key); // 插入 bool Find(const K& key); // 查找 void InOrder(void); // 中序遍历 bool IsBalance(void); // 检测是否满足红黑树规则 private: Node* _root; // 根节点指针 }; }
红黑树通过不断插入元素,构成二叉搜索树,若是破坏了红黑树的规则,则对树进行调整,直到构成红黑树
插入的新节点该是什么颜色呢?
新创建的节点一定是红色,因为新节点是黑色会导致本条路径的黑色节点数量多出一个,破坏红黑树的规则。
插入步骤:
当出现连续的红色节点时,当前节点为红色,父节点为红色,祖父节点一定为黑色,所以我们通过叔叔节点来判断当前红黑树的状况。
当新节点插入,父亲节点为红色时,叔叔节点只有2种情况:叔叔节点为红色或不存在
(1)叔叔节点为红色
当叔叔节点为红色时,需要将父亲节点和叔叔节点变成黑色,祖父节点变成红色,此时整颗子树每条路径的黑色节点仍然是一个,与其他路径的黑色节点数量一致。
当叔叔节点不存在时插入新节点,我们通过变色的方式调整为红黑树,此时子树的顶端被我们修改为了红色。
对于根节点问题,我们可以判断子树顶端是否为根节点,如果是则不改变根节点的颜色。也可以在插入完成时将根节点的颜色改为黑色。我们采用第二种方案,因为不需要判断,写起来方便
向上调整时,叔叔节点一定存在,若是叔叔节点还是红色,则继续变色,直到调整到根节点或叔叔节点为黑色
红色方框的意思是,该节点为红色或不存在
(2)叔叔节点为黑色或不存在
当叔叔节点为黑色或者不存在时,需要对树的形状进行调整,调整的方式就是旋转。
根据此时树的形状,将旋转分为4类,分别是左单旋,右单旋,左右双旋,右左双旋
当旋转完毕后要对对应的节点进行变色
黑色方框代表节点为黑色或不存在
<1>右单旋:
右单旋的过程:
- 让祖父节点g成为父节点p的右孩子
- 若是父节点p有右孩子,则将其交给祖父节点p领养,成为p的左孩子。(图中p没有右孩子)
- 此时父节点p在子树的最顶端,将其改为黑色。祖父节点g改为红色
代码实现
以下代码的实现与AVL树中的代码一样,只是没有平衡因子的更新。还有一个区别是这里的参数传递的是祖父节点,但是操作和AVL树中的父节点一样,可以理解成红黑树中的当前节点比AVL树中的矮一层
// 右单旋 void RotateR(Node* grandpa) { Node* grandpa_p = grandpa->_parent; // 祖父节点的父节点,用来连接旋转后的子树 Node* parent = grandpa->_left; // 父节点 Node* rightSun = parent->_right; // 父节点的右孩子 // 链接孩子节点 parent->_right = grandpa; grandpa->_left = rightSun; if (grandpa_p != nullptr) { // 祖父节点不为根节点 if (grandpa_p->_left == grandpa) { grandpa_p->_left = parent; } else { grandpa_p->_right = parent; } } else { // 祖父节点为根节点 _root = parent; } // 更新父节点 parent->_parent = grandpa_p; grandpa->_parent = parent; if (rightSun != nullptr) { rightSun->_parent = grandpa; } }
<2>左单旋
右单旋的过程:
- 让祖父节点g成为父节点p的左孩子
- 若是父节点p有左孩子,则将其交给祖父节点p领养,成为p的右孩子。(图中p没有左孩子)
- 此时父节点p在子树的最顶端,将其改为黑色。祖父节点g改为红色
代码实现:
// 左单旋 void RotateL(Node* grandpa) { Node* grandpa_p = grandpa->_parent; Node* parent = grandpa->_right; Node* leftSub = grandpa->_left; // 链接孩子节点 grandpa->_left = grandpa; grandpa->_right = leftSub; if (grandpa_p != nullptr) { if (grandpa_p->_left == grandpa) { grandpa_p->_left = grandpa; } else { grandpa_p->_right = grandpa; } } else { _root = grandpa; } // 更新父节点 grandpa->_parent = grandpa_p; grandpa->_parent = grandpa; if (leftSub != nullptr) { leftSub->_parent = grandpa; } }
<3>左右双旋
由于红黑树的左右双旋和右左双旋可以直接调用左单旋和右单旋,并且最后只需要更新节点颜色,不需要多余的操作,我们就不将这两个封装成为一个单独的函数了,使用时直接展开书写即可。
<4>右左双旋
这4种旋转方式得到的结果都有一个共同点:
子树的顶端一定是黑色节点,不可能再出现两个连续的红色节点,不需要再向上调整了
(1)构造函数
根节点指针_root
初始化为空指针即可
RBTree(void)
: _root(nullptr)
{
;
}
(2)析构函数
后序遍历依次释放每个节点,由于要使用递归,参数要传递节点指针,因此我们将递归过程单独封装成为一个函数,让析构函数调用它
// 析构函数 ~RBTree(void) { _Destroy(_root); _root = nullptr; } private: // 后序遍历释放节点 void _Destroy(Node* root) { if (root == nullptr) { return; } _Destroy(root->_left); _Destroy(root->_right); delete root; }
(3)拷贝构造
先序遍历依次拷贝每个节点,也将递归过程单独封装为一个函数
// 拷贝构造 RBTree(const AVLTree& t) { _root = _Copy(_root, t._root); } private: // 先序遍历递归拷贝节点 Node* _Copy(Node* root, const Node* src) { if (src == nullptr) { return nullptr; } root = new Node(src->_key); root->_left = _Copy(root->_left, src->_left); root->_right = _Copy(root->_right, src->_right); return root; }
(4)赋值重载
创建临时对象拷贝用来赋值的对象,然后交换自己和它的根节点指针内容,获取临时对象的数据。
// 赋值重载函数
RTree& operator=(const AVLTree& t)
{
if (this != &t)
{
BSTree temp(t);
std::swap(temp._root, _root);
}
return *this;
}
红黑树插入元素的步骤:
bool Insert(const pair<K, V>& kv) { // 1.以二叉搜索树的方式插入新节点 if (_root == nullptr) { _root = new Node(kv); _root->_col = BLACK; // 根节点为黑色 return true; } Node *cur = _root, *parent = nullptr; while (cur != nullptr) { 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); if (kv.first < parent->_kv.first) { parent->_left = cur; cur->_parent = parent; } else { parent->_right = cur; cur->_parent = parent; } // 2.调整为红黑树 while (parent != nullptr && parent->_col == RED) { Node* grandpa = parent->_parent; // 红色节点一定有父亲,所以不用担心空指针 Node* uncle = grandpa->_left; // 获取叔叔节点 if (parent == uncle) { uncle = grandpa->_right; } if (uncle != nullptr && uncle->_col == RED) { // 叔叔节点为红:变色 parent->_col = BLACK; uncle->_col = BLACK; grandpa->_col = RED; cur = grandpa; parent = cur->_parent; } else { // 叔叔节点为黑或不存在:旋转+变色 if (parent == grandpa->_left && cur == parent->_left) { // 右单旋 RotateR(grandpa); // 变色 parent->_col = BLACK; grandpa->_col = RED; } else if (parent == grandpa->_right && cur == parent->_right) { // 左单旋 RotateL(grandpa); // 变色 parent->_col = BLACK; grandpa->_col = RED; } else if (parent == grandpa->_left && cur == parent->_right) { // 左右双旋 RotateL(parent); RotateR(grandpa); // 变色 cur->_col = BLACK; grandpa->_col = RED; } else if (parent == grandpa->_right && cur == parent->_left) { // 右左双旋 RotateR(parent); RotateL(grandpa); // 变色 cur->_col = BLACK; grandpa->_col = RED; } else { assert(false); // 不会出现的情况,使用断言处理 } break; // 旋转之后,顶一定是黑,不需要再判断连续红节点了 } } _root->_col = BLACK; // 将根节点置为黑色 return true; }
就是二叉搜索树的查找方式进行查找
bool Find(const K& key) { Node* cur = _root; while (cur != nullptr) { if (key < cur->_key) { // 比当前节点小,往左走 cur = cur->_left; } else if (key > cur->_key) { // 比当前节点大,往右走 cur = cur->_right; } else { // 与当前节点相等,返回true return true; } } return false; }
递归实现中序遍历
// 中序遍历 void InOrder(void) { _InOrder(_root); std::cout << std::endl; } // 中序遍历的递归过程 void _InOrder(Node* root) { if (root == nullptr) { return; } _InOrder(root->_left); std::cout << root->_key << " "; _InOrder(root->_right); }
bool IsBalance(void) { // 空树是红黑树 if (_root == nullptr) { return true; } // 根节点为红色不是红黑树 if (_root->_col != BLACK) { return false; } // 获取最左边路径的黑色节点数目 int ref = 0; Node* left = _root; while (left != nullptr) { if (left->_col == BLACK) { ++ref; } left = left->_left; } return _IsBalance(_root, 0, ref); } private: // 递归遍历节点,并判断是否满足红黑树的规则 bool _IsBalance(const Node* root, int blackNum, const int ref) const { if (root == nullptr) { if (blackNum != ref) { cout << "路径黑色节点数跟最左路径不相等" << endl; return false; } return true; } // 判断是否有连续红色节点 if (root->_col == RED && root->_parent->_col == RED) // 红色节点一定有父亲,不担心空指针问题 { cout << "error : " << root->_kv.first << " and " << root->_parent->_kv.first << endl; return false; } if (root->_col == BLACK) { ++blackNum; } return _isBalance(root->_left, blackNum, ref) && _isBalance(root->_right, blackNum, ref); }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。