github地址:https://github.com/duchenlong/Cpp/tree/master/RB_tree/RB_tree
红黑树
什么是红黑树
红黑树,也是二叉搜索树的一种形式。
在红黑树中,每个节点上增加了一个节点的颜色(RED 和 BLACK)
。他通过对从根节点到叶子节点的路径上的各个节点的颜色,来判断是否是接近平衡的。
红黑树的性质
- 每一个结点不是红色就是黑色
- 根节点始终为黑色
- 如果一个节点是红色的,那么他的两个孩子节点以及父亲节点一定是黑色的(没有相连的红色节点)
- 对于每一个结点,从该节点到叶子节点的所有路径中,均包含相同数目的黑色节点
所以说,一个红黑树,他的最长路径中的节点数一定不会超过最短路径节点个数的两倍。
最长路径
:从根节点开始,就是一个黑节点,一个红色节点交替连接最短路径
:全部都是黑色节点的路径
对比AVL树:
时间复杂度
:AVL树的时间复杂度为O(log N)
,红黑树的时间复杂度为O(log 2 * N)
。AVL树中
,他是一个严格平衡的树,也就是当左右子树一旦出现不平衡的状态,就直接进行旋转操作。红黑树
中,只有当破坏了红黑树性质的条件时,才进行旋转调节操作。
所以说,AVL树
与红黑树
对比而言,他们的时间复杂度都是差不多的,只是AVL树存在着大量的旋转操作,比较费时间;而红黑树的旋转操作相对而言比较少。
红黑树节点的定义
- 节点的颜色
enum Color
{
BLACK,
RED,
};
在STL中对于节点的颜色采用的时候两个bool类型的变量
,红色节点为false,黑色节点为true。
- 节点的定义
template<class T>
struct RBTreeNode
{
typedef RBTreeNode<T> node;
node* _left;
node* _right;
node* _parent;
T _data;
Color _col;
//构造的节点为红色节点
RBTreeNode(const T& data)
{
_left = _right = _parent = nullptr;
_data = data;
_col = RED;
}
};
- 红黑树中类的定义
template<class K,class T,class KOfT>
class RBTree
{
public:
typedef RBTreeNode<T> node;
//迭代器
typedef __TreeIterator<T, T&, T*> iterator;
typedef __TreeIterator<T, const T&, const T*> const_iterator;
public:
RBTree()
:_root(nullptr)
{}
private:
node* _root;
};
其中,K表示键值,T表示树中节点的数据类型,一般是一个pair类型的结构体,KOFT是一个结构体,他的作用就是取出T结构体中的键值进行比较
一般KOFT的描述可以这样来,在他的里面必须重载()
template<class K,class V>
struct cmp
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
}
插入操作
pair<iterator,bool> Insert(const T& data)
{
}
在插入一个新的节点之前,我们需要考虑的是这个新插入的节点是什么颜色的?
- 由于红黑树的性质二 ,根节点始终为黑色
那么当插入节点为根节点的时候,就是黑色的节点
- 红黑树的性质四,对于每一个结点,从该节点到叶子节点的所有路径中,均包含相同数目的黑色节点
如果我们新插入一个黑色的节点,那么很容易就会破坏这个特性,所以说我们就让新插入的节点为红色节点
,这样对整体结构的影响比较小,然后再进行向上调整。
同AVL树,我们在插入的时候,先要找到待插入的这个节点的位置。
//找到插入节点
if (_root == nullptr)
{
_root = new node(data);
_root->_col = BLACK;
return make_pair(iterator(_root),true);
}
KOfT koft;//比较的函数
node* parent = nullptr;
node* cur = _root;
while (cur)
{
if (koft(cur->_data) == koft(data))
return make_pair(iterator(cur),false);
parent = cur;
cur = (koft(cur->_data) < koft(data)) ? cur->_right : cur->_left;
}
//此时cur为插入节点的位置,cur即为nullptr
cur = new node(data);
node* newNode = cur;
if (koft(parent->_data) < koft(cur->_data))
{
parent->_right = cur;
cur->_parent = parent;
}
else if (koft(parent->_data) > koft(cur->_data))
{
parent->_left = cur;
cur->_parent = parent;
}
接下来就是调节的过程了,对于调节而言,因为我们的新插入的节点为红色,那么当他的父亲节点也是红色的时候,就需要进行调节了。
//调节完毕的条件
while (parent && parent->_col == RED)
对于调整,我们需要根据叔叔节点来判断调整的方式。为了找到叔叔节点(u)
,我们就需要祖父节点的帮助(g)
先判断,新插入的节点在祖父节点的左子树中的情况
- 叔叔节点存在,并且为红色节点
// 1. uncle 存在,并且该节点为红色
if (uncle != nullptr && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//继续向上处理
cur = grandfather;
parent = cur->_parent;
}
继续向上处理时,我们需要保证cur为红色节点,也就是祖父节点(g)。
- 叔叔节点不存在,那么cur节点一定是新增的节点(因为在插入这个节点之前,这棵树就是一个红黑树)
如果叔叔节点存在,那么在这个分支中,叔叔节点一定是一个黑色节点,我们同样也进行一次右旋操作
直接对祖父节点进行右旋,然后变色就可以了,这个时候就可以退出循环了。
Left_Left(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
- 当前节点(cur)是 parent 节点的右子树节点
//3.cur 是parent的右子树节点
if (cur == parent->_right)
{
Right_Right(parent);
swap(parent, cur);//单纯的交换两个指针的位置,让他变成第二种情况
}
//2.uncle节点为nullptr,说明cur结点一定是新增节点,而不是向上调整后的节点
//uncle节点存在,那么此时他一定是黑色节点
//对grandfather进行一次右旋
Left_Left(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
break;
同理,当新插入的节点是祖父节点的右子树时
当需要旋转的时候,
else if (grandfather->_right = parent)
{
node* uncle = grandfather->_left;
if (uncle != nullptr && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
Left_Left(parent);
swap(parent, cur);
}
Right_Right(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
break;
}
}
右旋
//右旋
void Left_Left(node* parent)
{
node* pParent = parent->_parent;
node* pLeft = parent->_left;
node* pLRight = pLeft->_right;
parent->_left = pLRight;
if (pLRight) pLRight->_parent = parent;
pLeft->_right = parent;
parent->_parent = pLeft;
if (pParent == nullptr)
_root = pLeft;
else
(parent == pParent->_left) ? pParent->_left = pLeft: pParent->_right = pLeft;
pLeft->_parent = pParent;
}
左旋
//左旋
void Right_Right(node* parent)
{
node* pParent = parent->_parent;
node* pRight = parent->_right;
node* pRLeft = pRight->_left;
parent->_right = pRLeft;
if(pRLeft) pRLeft->_parent = parent;
pRight->_left = parent;
parent->_parent = pRight;
if (pParent == nullptr)
_root = pRight;
else
(pParent->_left == parent) ? pParent->_left = pRight : pParent->_right = pRight;
pRight->_parent = pParent;
}
红黑树的检测
对于红黑树的检测,我们需要严格根据红黑树的性质进行判断
- 根节点为黑色
- 对于每一个结点,从该节点到叶子节点的所有路径中,均包含相同数目的黑色节点
- 没有相连的红色节点
//判断是否是红黑树
bool IsRBTree()
{
node* proot = _root;
//空树也是红黑树
if (nullptr == proot)
return true;
//根节点必须为黑色
if (BLACK != proot->_col)
{
cout << "根节点不为黑色" << endl;
return false;
}
//获取任意一条路径中,黑色节点的数量
size_t blackNum = 0;
node* cur = proot;
while (cur != nullptr)
{
if (BLACK == cur->_col)
blackNum++;
cur = cur->_left;
}
size_t k = 0;
return _IsRBTree(proot, k, blackNum);
}
我们在第一个函数中,先找到一条路径上的黑色节点数量,然后在判断这棵树是否满足所有路径的黑色节点数量相等
private:
bool _IsRBTree(node* root, size_t k, size_t blackNum)
{
//走到了叶子节点,判断黑色节点是否相等
if (nullptr == root)
{
if (k != blackNum)
{
cout << "路径中黑色节点数量不相等" << endl;
return false;
}
return true;
}
// 统计黑色节点的个数
if (BLACK == root->_col)
k++;
// 检测当前节点与其双亲是否都为红色
node* pParent = root->_parent;
if (pParent != nullptr && RED == pParent->_col && RED == root->_col)
{
cout << "有两个相连的红色节点" << endl;
return false;
}
return _IsRBTree(root->_left, k, blackNum) && _IsRBTree(root->_right, k, blackNum);
}
红黑树中的迭代器
跟list容器一样,我们对于迭代器的功能进行了一下封装,组成一个迭代器的类(这个类在红黑树类中得是public的,因为map和set的底层还需要使用这个迭代器)
- 迭代器的框架
Ref为数据的引用类型,Ptr为数据的指针类型
template<class T,class Ref,class Ptr>
struct __TreeIterator
{
typedef RBTreeNode<T> Node;
typedef __TreeIterator<T, Ref, Ptr> Self;
Node* _node;
__TreeIterator(Node* node)
:_node(node)
{}
};
- 自增操作的重载(++)
首先,我们需要知道,自增操作的每一步都是找一个比当前节点数据大的节点,这就被分成了两步
第一步,如果当前节点存在右子树,那么自增的下一步就是右子树中最小的节点
//如果存在右子树,那么下一个节点就是右子树中最小的那个节点
if (_node->_right)
{
Node* cur = _node->_right;
while (cur->_left)
cur = cur->_left;
_node = cur;
}
第二步,如果当前节点的右子树不存在,那么就需要向上找第一个没有被访问的节点
else
{
//右子树不存在,
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_right)
{
cur = cur->_parent;
parent = parent->_parent;
}
_node = parent;
}
- 自减操作的重载(- -)
同理,只是在自减的操作中,需要找的是下一个比自己小的节点,那么就需要先判断左子树,其他步骤同自增
Self& operator--()
{
if (_node->_left != nullptr)
{
//找到左子树中最大的一个节点
Node* cur = _node->_left;
while (cur->_right)
cur = cur->_right;
_node = cur;
}
else
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
- *,->,==,!=,运算符重载
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
- 红黑树中的begin,end函数
//迭代器
iterator begin()//返回最小的节点的iterator迭代器
{
node* cur = _root;
while (cur && cur->_left)
cur = cur->_left;
return iterator(cur);
}
iterator end()//返回最大元素的下一个元素,即为空
{
return iterator(nullptr);
}
STL中 map的实现
map类的框架
template<class K,class V>
class map
{
//比较的函数
struct MapKOfV
{
const K& operator()(const pair<K, V>& data)
{
return data.first;
}
};
typedef typename RBTree<K, pair<K, V>, MapKOfV> RB_type;
typedef typename RB_type::iterator iterator;
private:
RB_type _t;//红黑树
};
在map的底层,其实是调用的红黑树的接口,所使用的数据都是通过红黑树来存储的。
- []运算符重载
在[]
运算符中,他的内部其实是使用了一次插入的操作,不管该节点是否存在,我们直接对他进行插入操作。
只是在插入的时候,我们只知道键值,而不知道所存储的数据,所以我们对于pair结构体的第二个数据,直接调用这个数据类型的构造函数,创建一个匿名对象。
V& operator[](const K& k)
{
pair<iterator, bool> ans = _t.Insert(make_pair(k, V()));
return ans.first->second;
}
对于他的返回值问题,我们需要返回的是,这个红黑树节点(ans.first)的第二个数据(ans.first->second)的引用。
其他的函数没有什么可说的,就是调用的红黑树的接口。