目录
1. 红黑树的概念
2. 红黑树的性质
3. 红黑树节点的定义
4. 红黑树结构
5. 红黑树的插入操作
1. 按照二叉搜索的树规则插入新节点
2. 插入后看颜色是否符合要求
情况1:c为红,p为红,g为黑,u存在且为红
情况2:c为红,p为红,g为黑,u不存在/u存在且为黑
情况3:c为红,p为红,g为黑,u不存在/u存在且为黑
1. 以升序插入构建红黑树
6. 红黑树的验证
7. 红黑树与AVL树的比较
8. 红黑树的应用
9. 红黑树模拟实现STL中的map与set
9.1 红黑树的迭代器
9.2 改造红黑树
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的 (也就是说不能有连续的红色节点)
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点 (每条路径(左右子树)的黑色节点相同)
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
这里为什么节点的颜色默认是红色的?
因为节点如果默认是黑色,那么只要新增节点就一定会破坏红黑树的第四个条件(每个路径上的黑色节点相同),而节点默认是红色,则有概率插入到正确的位置,即使出现连续的红色节点,我们也可以通过调整节点的颜色来使红黑树恢复正常。
enum Color
{
RED,
BLACK
};
template
struct RBTreeNode
{
RBTreeNode(const pair& Data)
: _Data(Data)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_col(RED) //默认是红色
{}
pair _Data;
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
Color _col; //节点的颜色
};
template
class RBTree
{
typedef RBTreeNode Node;
private:
Node* _root = nullptr;
};
这里和stl库中的红黑树结构稍有不同,库里为了实现关联式容器简单,红黑树的实现中增加一个头结点,因为跟节点必须为黑色,为了 与根节点进行区分,将头结点给成黑色,并且让头结点的 pParent 域指向红黑树的根节点,pLeft 域指向红黑树中最小的节点,_pRight域指向红黑树中最大的节点,如下图:
注: c -> cur, p -> parent, g -> grandParent, u -> uncle,
如果插入后出现情况一,那么这时就需要对节点进行更改颜色以达到红黑树的条件要求,将p和u的颜色改为黑色,将g的颜色改为红色,g如果是根节点,那么会在调整结束后对g进行修改颜色;如果g是子树,那么g肯定有父亲,g的父亲还是红色,那么把g当成c,继续向上调整。
情况1变情况1:
当然情况1也有可能是通过子树调整后得到的,如下图,插入c后出现了连续的红色节点,出现了情况1,那么就需要将p和u的颜色变为黑,g的颜色变为红色。继续向上更新位置,可以看到又出现了情况1,那么继续调整颜色,将p和u的颜色变为黑色,g的颜色变为红色,调整完成后发现不符合循环的条件了,那么调整结束,最后将根的颜色改为黑色,就符合红黑树的要求了。
u的情况分两种:1. u不存在,那么c一定是新增,2. u存在且为黑,那么c原来的颜色是黑,只不过是由于c的子树调整时将c改成了红色。
出现情况2后,更改颜色并不能满足红黑树的要求了,所以此时进行旋转操作,以下图为例,u不存在,直接跟AVL树一样右旋即可,旋转完成后将g改为红色,p改为黑色,旋转完成后也就不需要再进行调整了,直接break即可。
以下图为例,出现情况3时,跟AVL树一样,g,p,c连成的线是一条曲线,单旋只能将曲线 水平翻转 ,而不能满足要求,那么就要双旋,先以p为轴点向左旋转,再以g为轴点向右旋转,旋转完成后再讲g改成红色,c改成黑色,调整就完成了。
情况1变情况3:
以下图为例,情况1调整后变成情况3,情况3以p为轴点进行左单旋,调整后变成情况2,再以g为轴点进行右单旋,调整完成后红黑树就符合条件啦。
当然上图只是一部分例子,还有不同的各种各样的例子出现,比如上面的p是g的左树,而p如果是g的右树,只需要对应情况对代码做一点修改即可。
bool Insert(const pair& value)
{
if (_root == nullptr)
{
_root = new Node(value);
_root->_col = BLACK;
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_Data.first > value.first)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_Data.first < value.first)
{
parent = cur;
cur = cur->_right;
}
else
{
return false;
}
}
cur = new Node(value);
if (parent->_Data.first > value.first)
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
while ( parent && parent->_col == RED)
{
Node* grandParent = parent->_parent;
if (parent == grandParent->_left)
{
Node* uncle = grandParent->_right;
//情况一
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandParent->_col = RED;
cur = grandParent;
parent = cur->_parent;
}
else
{
//情况二
if (parent->_left == cur)
{
RotateR(grandParent);
grandParent->_col = RED;
parent->_col = BLACK;
}
else //情况三
{
RotateL(parent);
RotateR(grandParent);
grandParent->_col = RED;
cur->_col = BLACK;
}
break;
}
}
else
{
Node* uncle = grandParent->_left;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandParent->_col = RED;
cur = grandParent;
parent = cur->_parent;
}
else
{
if (parent->_right == cur)
{
RotateL(grandParent);
grandParent->_col = RED;
parent->_col = BLACK;
}
else
{
RotateR(parent);
RotateL(grandParent);
grandParent->_col = RED;
cur->_col = BLACK;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
动态效果演示:
2. 以降序插入构建红黑树
验证一棵树是否是红黑树,要 检测其是否满足红黑树的性质。
- 空树也是红黑树;
- 根节点必须是黑色的;
- 不能有连续的红色节点,如果当前节点为红色,那么我们去检查他的父亲是否为红色,如果是则证明这棵树不是红黑树,这里为什么不去检查节点的孩子呢?因为节点的孩子是不确定的,我们不知道他是否有左孩子或或者右孩子,但是他一定有父亲节点。
- 每条路径上的黑色节点相同,先记录一条路径的黑色节点的个数作为基准值,再统计每条路径的黑色节点的个数进行判断,看是否符合要求。
bool IsBalance()
{
//空树也是红黑树
if (_root == nullptr)
return true;
//根节点的颜色必须为黑色
if (_root->_col != BLACK)
return false;
//拿最左路径的黑色节点,作为每条路径黑色节点的参考值
int ref = 0;//黑色节点参考值
Node* cur = _root;
while (cur)
{
if(cur->_col == BLACK)
ref++;
cur = cur->_left;
}
return _IsBalance(_root,0,ref);
}
//用blacknum 记录每条路径的黑色节点的个数
bool _IsBalance(Node* root,int blacknum,const int& ref)
{
if (root == nullptr)
{
//每条路径上的黑色节点相同
if (blacknum != ref)
{
cout << "异常:当前路径的黑色节点与参考值不同" << endl;
return false;
}
return true;
}
//不能有连续的红色节点
if (root->_col == RED && root->_parent->_col == RED)
{
cout << root->_Data.first<<"异常:出现了连续的红色节点" << endl;
return false;
}
//统计黑色节点的个数
if (root->_col == BLACK)
++blacknum;
return _IsBalance(root->_left, blacknum, ref)
&& _IsBalance(root->_right, blacknum, ref);
}
1. C++ STL库 -- map/set、mutil_map/mutil_set
2. Java 库
3. linux内核
4. 其他一些库
这里我实现的与stl库中的迭代器稍有不同,库中的红黑树添加了一个哨兵位的头节点header,header的左指针指向树中的最小值(最左节点),header的右指针指向树中的最大值(最右节点)。begin()与end()代表的是一段前闭后开的区间,而通过中序遍历可以得到一个有序的序列,stl库里的begin()是最左节点,end()是header自己。
因为我实现的红黑树没有哨兵位的头结点,所以我的begin()先找到最左节点,再用节点构造一个迭代器返回,中序遍历走到根的父亲就结束(表示左子树,根和右子树都已经走完了),所以end()为空。
typedef __RBtree_Iterator iterator;
iterator begin()
{
//找最左节点
Node* left = _root;
while (left->_left)
{
left = left->_left;
}
//用迭代器构造一个返回
return iterator(left);
}
iterator end()
{
return iterator(nullptr);
}
那么有了begin()和end(),还差一个迭代器的类,这里要注意的就是++和--,我们以++为例(走的是中序遍历,因为中序遍历会得到一个有序的序列),分两种情况:
1.右不为空,那么说明左子树和根走完了,那么就要找右子树的最小节点,找到后将min的给node,就相当于++走到了下一个;
2.右为空,那么说明子树的左子树,根和右子树都走完了,需要向上找孩子是父亲的左的那个父亲,找到了就把parent给node,就相当于找到这颗子树的父亲,有个特殊情况需要处理,如果已经走到最右节点,那么迭代器再++就该结束了,cur和parent会一直向上更新,直到parent为空,再把parent给node,那么迭代器就结束了。
--只需用++反向推导即可。
template
struct __RBtree_Iterator
{
typedef RBTreeNode Node;
typedef __RBtree_Iterator self;
Node* _node;
__RBtree_Iterator( Node* node)
:_node(node)
{}
Ref operator*()const
{
return _node->_Data;
}
Ptr operator->()const
{
return &_node->_Data;
}
self& operator++()
{
//右不为空,就找右边的最左节点
if (_node->_right)
{
Node* min = _node->_right;
while (min->_left)
{
min = min->_left;
}
_node = min;
}
else//右为空,要向上找孩子是父亲的左的那个父亲
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && parent->_right == cur)
{
cur = parent;
parent = cur->_parent;
}
_node = parent;
}
return *this;
}
self& operator--()
{
if (_node->_left)
{
Node* mix = _node->_left;
while (mix->_right)
{
mix = mix->_right;
}
_node = mix;
}
else
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && parent->_left == cur)
{
cur = parent;
parent = cur->_parent;
}
_node = parent;
}
return *this;
}
bool operator!=(const self& node) const
{
return _node != node._node;
}
bool operator==(const self& node) const
{
return _node == node._node;
}
};
因为我们后面要用红黑树一个类封装出map和set两个对象,所以要对红黑树的结构稍作修改。
template
class map
{
struct MapKeyOfT
{
//提取key值
const K& operator()(const pair& kv)
{
return kv.first;
}
};
bool insert(const pair& kv)
{
return _t.Insert(kv);
}
private:
RBTree, MapKeyOfT> _t;
};
template
class set
{
struct SetKeyOfT
{
//提取key值
const K& operator()(const K& key)
{
return key;
}
};
bool insert(const K& key)
{
return _t.Insert(key);
}
private:
RBTree _t;
};
关联式容器中存储的是的键值对,因此 k 为key的类型,value我用T来表示 T: 如果是map,则为pair; 如果是set,则为key, KeyOfT: 通过T来获取key的一个仿函数类。
template
struct RBTreeNode
{
RBTreeNode(const T& Data)
: _Data(Data)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_col(RED) //默认是红色
{}
T _Data;
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
Color _col; //节点的颜色
};
template
class RBTree
{
public:
typedef RBTreeNode Node;
typedef __RBtree_Iterator iterator;
typedef __RBtree_Iterator const_iterator;
public:
iterator begin()
{
Node* left = _root;
while (left->_left)
{
left = left->_left;
}
return iterator(left);
}
const_iterator begin()const
{
Node* left = _root;
while (left->_left)
{
left = left->_left;
}
return const_iterator(left);
}
iterator end()
{
return iterator(nullptr);
}
const_iterator end()const
{
return const_iterator(nullptr);
}
private:
Node* _root = nullptr;
那么有了仿函数还需要对插入做一些修改,先实例化一个仿函数对象,再把以值进行比较的地方做一点修改。
那么插入实现了,得看看插入的值对不对呀,所以再把map和set的迭代器再封装一个。这里有个前提条件是不能通过迭代器去修改map和set中的key值,因为修改以后他这个红黑树可能就不符合要求了,所以使用的时候是不能通过迭代器去修改key值,但是map可以通过迭代器去修改second的值,因为map要进行统计。
看下图,stl库中set的迭代器底层全都是const迭代器,而map中普通就是迭代器,const就是const。既然map的迭代器是正常的,那为什么map中的key值也不能修改呢?那是因为在定义的时候pair里的key值就加了const,所以不能修改,大家可以试一试把pair里的const去掉看一看key值能不能修改。
在set这里没有提供两个版本的迭代器,只提供了普通的迭代器,但这个普通迭代器底层也是const迭代器。为什么他在这里给函数加了一个const就行了?因为函数加了const,const对象可以调用,普通对象也可以调用,普通对象调用权限缩小,就让这里的 t强行变成const对象,那么const对象调用这个迭代器也就可以了。
当然map就需要实现两个版本喽。
class set
{
public:
typedef typename RBTree::const_iterator iterator;
typedef typename RBTree::const_iterator const_iterator;
iterator begin()const
{
return _t.begin();
}
iterator end()const
{
return _t.end();
}
private:
RBTree _t;
};
class map
{
public:
typedef typename RBTree, MapKeyOfT>::iterator iterator;
typedef typename RBTree, MapKeyOfT>::const_iterator const_iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
const_iterator begin()const
{
return _t.begin();
}
const_iterator end()const
{
return _t.end();
}
private:
RBTree, MapKeyOfT> _t;
};
通过上面一顿输出,set和map就可以简单使用啦!
那么到这map还想通过[ ]实现统计次数,那么库中map的[ ]是通过插入k值,插入后返回一个带有迭代器的pair,然后pair取到迭代器,再通过迭代器取到second,然后返回。
[ ]在前面的文章里讲过,如果这个值不存在那么就插入成功,如果存在就返回所在节点的迭代器。要支持这个[ ]就需要对insert的返回类型和返回值进行修改,返回类型不用说,一样的,返回值就用当前节点构造一个迭代器返回。
pair Insert(const T& Data)
{
if (_root == nullptr)
{
_root = new Node(Data);
_root->_col = BLACK;
return make_pair(iterator(_root),true);//通过root节点构造一个迭代器
}
KeyOfT kot;
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (kot(cur->_Data) > kot(Data))
{
parent = cur;
cur = cur->_left;
}
else if (kot(cur->_Data) < kot(Data))
{
parent = cur;
cur = cur->_right;
}
else
{
return make_pair(iterator(cur), false);
}
}
cur = new Node(Data);
Node* newnode = cur;//保存新插入的节点,后面要用这个节点构造迭代器,cur会随着循环丢失位置
cur->_col = RED;
if (kot(parent->_Data) > kot(Data))
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//如果parent的颜色是红色那么就继续调整,是黑色就结束
while ( parent && parent->_col == RED)
{
Node* grandParent = parent->_parent;
if (parent == grandParent->_left)
{
Node* uncle = grandParent->_right;
//情况一
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandParent->_col = RED;
cur = grandParent;
parent = cur->_parent;
}
else
{
//情况二
if (parent->_left == cur)
{
RotateR(grandParent);
grandParent->_col = RED;
parent->_col = BLACK;
}
else //情况三
{
RotateL(parent);
RotateR(grandParent);
grandParent->_col = RED;
cur->_col = BLACK;
}
break;
}
}
else
{
Node* uncle = grandParent->_left;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandParent->_col = RED;
cur = grandParent;
parent = cur->_parent;
}
else
{
if (parent->_right == cur)
{
RotateL(grandParent);
grandParent->_col = RED;
parent->_col = BLACK;
}
else
{
RotateR(parent);
RotateL(grandParent);
grandParent->_col = RED;
cur->_col = BLACK;
}
break;
}
}
}
_root->_col = BLACK;
return make_pair(iterator(newnode), true);
}
insert修改完成后,再把[ ]实现了,这里红黑树结构里不用实现[ ],在map里封装一个就可以。在[ ]里调用insert,插入key值和value,value就用匿名函数(调用自己的默认构造),返回时先取到迭代器,再用迭代器找second。那么map里的insert也相应的修改一下。
//map
pair insert(const pair& kv)
{
return _t.Insert(kv);
}
V& operator[](const K& key)
{
pair ret = insert(make_pair(key, V()));
return ret.first->second;
}
set这里如果再像map一样写那么就会报错,为什么呢?还是那个原因,普通对象调用const迭代器当然会出错,所以这里不能直接用iterator,而要用红黑树里的iterator,先接收再构造一个pair返回。
//set
pair insert(const K& key)
{
//return _t.Insert(key);//会报错
pair::iterator, bool> ret = _t.Insert(key);
return pair(ret.first, ret.second);
}
如果直接像map一样 ,就会出现下图的报错,无法把普通对象转换为const对象。
那么要想让set的insert正常使用还需要给红黑树里的迭代器类里加下面这样的函数。下面的函数是什么意思呢?如果你是普通迭代器那么就是一个拷贝构造,如果你是一个const迭代器那么就是一个构造函数,用普通迭代器构造一个const迭代器。
//参数不是self而是迭代器
typedef __RBtree_Iterator iterator;
__RBtree_Iterator(const iterator& s)
:_node(s._node)
{}
注:这里进行修改时,要一步一步的来,肯定会有很多问题出现,而模板的报错又特别恶心,所以要多注意。
完整代码:RBTree/RBTree/RBTree.h · 晚风不及你的笑/作业库 - 码云 - 开源中国 (gitee.com)