目录
红黑树的基本编写
红黑树的概念
红黑树的性质
红黑树节点的定义
红黑树的插入操作
情况1.1
情况2
情况3:
判断平衡
测试
用红黑树封装map和set
迭代器
【operator++】
operator--
operator!=
map和set封装迭代器
find迭代器查找
对红黑树中Insert的改变
拷贝构造问题
赋值重载
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的
假设每条路径的黑节点数量是N,由此可以得知:任意路径长度>=N && 任意路径长度<=2N,这就说明虽短路径是全黑的,最长路径是一黑一红,这也证明最长路径不超过最短路径的2倍。
所以AVL树严格平衡,效率是logN(以2为底),红黑树是近似平衡,效率是2*logN。虽然红黑树效率没有AVL树高,但是对于现阶段的计算机而言,这点效率已经可以忽略不记。并且AVL树在达到平衡的目的下,需要进行旋转,而红黑树没有到最短路径2倍是不用旋转的,AVL树的旋转次数多,使效率降低,所以更推荐用红黑树。
红黑树的颜色我们用枚举定义,节点的定义用一个三叉链,构造节点,随便给一个颜色,
enum Color
{
RED,
BLACK
};
template
struct RBTreeNode
{
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
Color _col;
pair _kv;
RBTreeNode(const pair& kv)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED)
, _kv(kv)
{}
};
红黑树也是一个二叉搜索树,所以插入值的时候,如果值存在返回false,如果不存在,比当前节点大就向右边插入,如果比当前节点值小,向左边插入。
红黑树右两种颜色,红和黑。对于插入操作,我们因该插入哪个颜色的节点呢?如果插入黑色,那么这条路径的黑色节点数量就会改变,使得每条路径都要增加黑色节点,代价太大了,所以我们选择插入红色节点。
【插入红色节点分以下几种情况】:
约定:cur为当前节点,g为祖父节点,u为叔叔节点
:cur是新增为红,parent为红,grandfather为黑,uncle叔叔节点存在且为红。前三条是固定条件,唯一要看的就是uncle的情况。
解决方式:将p,u改为黑,g改为红,然后把g当cur,继续向上调整
因为新增了红,且有两个连续红节点,所以要向上更新parent节点的颜色为黑,uncle节点也要变成黑色。因为grandparent的节点不一定是根节点,如果不是根节点,需要将grandparent的节点颜色变成红色,来保证每条路径的黑色节点数量相同。当grandparent节点变成红色后,可能又会出现两个连续的红色节点,那么就衍生出第二种情况。
情况1.2:cur不是新增红色节点,parent为红,grandfather为黑,uncle叔叔节点存在且为红。
这种情况和情况1类似,只是cur不是新增节点,而是本就存在的节点。它最开始是有图1变换而来的,最后还是通过情况1的解决方法,也就是将情况1的grandparent当成cur,将现在的parent和uncle节点变黑,grandparent节点变红。如果g节点是根节点,就再次变黑。只要uncle存在且parent和uncle都为红,就会一直这样处理。
:uncle不存在 || uncle存在且为黑
解决方案:parent为grandfather的左孩子,cur为p的左孩子,则进行右单旋;相反,如果parent为grandfather的右孩子,cur为parent的右孩子,则进行左单旋。parent和grandfather变色:parent变黑,grandfater变红。
uncle不存在的情况下,如上图,如果没有uncle,那么只有一个黑节点,并且增加一个新的节点cur后,左树的路径长度超过了右树路径长度的2倍(从grandfather节点算起)。那么此时不单纯是改变节点颜色的问题,在AVL树中,左右子树高度差大于2,会进行旋转操作。
红黑树中,uncle节点不存在或者为黑的情况,会进行旋转,旋转的方法和AVL树一样:将parent节点的右子树给给grandfather的左子树,grandfather变成parent的右子树,此时这棵树高度降低,但是根节点颜色需要改变,使每条路径的黑色节点数量一样,parent节点变成黑色后,右边路径不能有两个黑节点,grandfather节点要变成红色。
我们再来看看uncle存在且为黑的情况,由图一变换到图二经历了情况1,也就是uncle为红的情况。再次循环,grandparent变成cur的时候,此时是第二种情况,uncle颜色为黑,这种情况需要右旋grandfather节点。还是一样的parent的左边给给grandparent的左边,grandparent变成parent的右边,最后将根节点parent和grandparent的颜色改变。这时候发现,这棵树不仅平衡了,而且每条路径的黑色节点数也一样。
至此,旋转完这棵树,就可以跳出循环了,因为最后这颗子树的根节点是黑色的,不需要继续向上处理,在第一种情况中,因为parent节点是红色的,cur也为红,两个连续的红色,需要进行处理。但是旋转完之后最后没有两个连续红节点,其它节点也根我没有关系。直接break跳出循环就可以了。
以上都是左树高的情况,而parent链接在grandfather的左边还是右边都无所谓,无论是左树高还是右树高,如果uncle为红,那么只需要改变它的颜色;如果uncle不存在或存在且为黑,那么左树高还是右树高是右区别的,只需要关注旋转方向。其代码大致类似。
但是还有一种情况,上面的单旋是处在cur和parent都是一边的情况。
如果出现,parent是grandfather的左边,cur是parent的右边,出现了一个折现,通过单旋并不能解决问题。需要先旋转parent位置的节点,进行左旋;然后两个红节点都变成一边后,再旋转grandfather节点,进行右旋,这是左右双旋的情况。如果parent是grandfather的右边,cur是parent的左边,就需要右左双旋,先右旋parent节点,两个红节点都在右边后,再对grandfather进行左旋。最后双旋完成后发现,黑色节点都在同一层树上。
这里我们只画右左双旋情况。
bool Insert(const pair& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
Node* cur = _root;
Node* parent = nullptr;
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;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//控制平衡
//如果父亲不存在,或者父亲的颜色是黑色,不需要处理
//所以这里的循环条件是:当grandparent给给cur,cur的父亲需要存在,且为红色
while (parent && parent->_col == RED)//循环条件:parent存在且parent为红
{
Node* grandfather = parent->_parent;
//grandfather的左边
if (parent == grandfather->_left)//左树高
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)//叔叔存在且为红,左树和右树都一样
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//叔叔不存在 或 叔叔为黑,需要关心左树高还是右树高,因为要旋转
{
if (cur == parent->_left)//单旋
{
RotateR(grandfather);//旋转
parent->_col = BLACK;
grandfather->_col = RED;
}
else//cur==parent->_right//双旋
{
RotateL(parent);
RotateR(grandfather);
//改变颜色
cur->_col = BLACK;
grandfather->_col = RED;
}
break;//这里可以直接break出去,不写的话也可以,再次进入循环,parent节点已经不是红色,进不去循环也会跳出去。
}
}
//grandfather的右边
else//parent=grandfather->_right;
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//叔叔不存在||为黑
{
if (cur == parent->_right)//单旋
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else//cur=parent->_left--RotateRL--双旋
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
左旋和右旋
void RotateL(Node* parent)//左旋
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
Node* grandparent = parent->_parent;
if (_root == parent)
{
_root = subR;
subR->_parent = nullptr;
}
else//parent不是根节点
{
if (grandparent->_left == parent)
grandparent->_left = subR;
else
grandparent->_right = subR;
subR->_parent = grandparent;
}
subR->_left = parent;
parent->_parent = subR;
}
void RotateR(Node* parent)//右旋
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* grandparent = parent->_parent;
//换根节点
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (grandparent->_left == parent)
grandparent->_left = subL;
else
grandparent->_right = subL;
subL->_parent = grandparent;
}
//parent变成subL的右子树
subL->_right = parent;
parent->_parent = subL;
}
判断一颗红黑树是否正确,要根据它的五个性质来判断,最重要的是3,4点:是否存在连个连续红色节点;每条路径的黑色节点数量是否相同。其它的我们还要判断:根节点是不是黑色。而只是评判最长路径是最短路径的二倍,并不能判断平衡。
判断连续的红色节点,我们采用当前节点和父亲节点的颜色相比较,如果使用当前节点和子节点比较,可能它的子节点不存在,或者只存在一个,要比较很多次,非常麻烦。所以如果当前节点存在,父节点一定存在,比较两个节点颜色,如果都是红色,说明出错了。
那么怎么查看每条路径的黑色节点相同呢?这里我们可以让参数里面带一个形参来记录节点的数量,这样每次递归调用的栈帧中都会有这个形参,会记录该路径的黑色节点数,并且我们不需要在节点的结构体中增加成员变量。在这里我们在主函数搞一个基准值,这个基准值是某一条路径的黑色节点数量,那么该基准值路径的黑色节点数量本身就是错误的怎么办?这个我们不需要关心,因为每个路径的黑色节点数量都是一样的,无论谁错了,那么整个红黑树都是不对的。所以我们就用最左路径的黑色路径作为基准值banchmark。
bool IsBlacne()
{
if (_root && _root->_col == RED)//如果根节点存在且为红,出错了
{
cout << "根节点不是黑色" << endl;
return false;
}
int banchmark = 0;
Node* left = _root;
while (left)
{
if (left->_col = BLACK)
++banchmark;
left = left->_left;
}
int blackNum = 0;//如果是空树,最开始每条路径的黑节点都是0
return _IsBlance(_root, banchmark, blackNum);
}
//子函数:遍历整个红黑树
bool _IsBlance(Node* root, int banchmark, int blackNum)
{
if (root == nullptr)//空树认为是一个红黑树
{
if (banchmark != blackNum)//走到NIL叶子节点,就判断该路径节点数量和基准值是否相同,不相同就返回false
{
cout << "存在路径黑色节点的数量不相等" << endl;
return false;
}
return true;
}
//对当前节点操作
//规则3:是否有两个连续红节点
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "出现连续的红色节点" << endl;
return false;
}
//规则4:每条路径黑节点数量是否相同
if (root->_col == BLACK)
++blackNum;
return _IsBlance(root->_left, banchmark, blackNum)
&& _IsBlance(root->_right, banchmark, blackNum);//递归遍历整个树
}
通过红黑树插入操作,将a数组中的值插入到红黑树中,,并对插入的每一个值做一个判断,是否是红黑树,打一个日志。最后总体做一个是否是红黑树的判断。
void TestRBTree()
{
RBTree t;
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto e : a)
{
t.Insert(make_pair(e, e));
cout << "Insert" << e << ":" << t.IsBlacne() << endl;
}
cout << t.IsBlacne() << endl;
}
如果觉得这个测试用例过于简单,可以写一个10000以内的随机值,进行插入。
void TestRBTree()
{
RBTree t;
vector v;
srand(time(0));
int N = 10000;
for (int i = 0; i < N; i++)
{
v.push_back(rand());
}
for (auto e : v)
{
t.Insert(make_pair(e, e));
if (!t.IsBlacne())//打印之前就检查,如果出错,在InOrder遍历之前就打印出来
{
cout << "Insert" << e << endl;
}
}
t.InOrder();
cout << t.IsBlacne() << endl;
}
这里调用递归不会崩溃的,因为红黑树的高度不高,递归层数不深,因为是一个相对平衡二叉树,在最平衡的情况下,10亿个数也就30层(2^30≈10亿)。可以测试以下,上面的10000个数是多少层,因为最长路径可以是最短路径的2倍,所以10000个随机数中可以有24层,也可能是16。如果是AVL数这种非常平衡的,10000个数也就14层。
在stl源码中,我们发现map和set都是用到红黑树来封装的,但是并没有写两份代码,map和set都是用到的RBTree这一份代码。但是它们比较大小的时候用的并不是同一类型。set用的是key来比较,map用的是pair来比较。在上面我们写的都是用key来比较,我们看到源码中,set和map都用到了rb_tree,所以rb_tree中会增加第二个模板参数,map和set实际上是用第二个模板参数来比较的。
那么set要比奥是用key来比较,map要比较是用pair来比较,如以下文档,我们挑了其中一个,可以发现,first和second只要有一个小,就是小的。但是我们只要first小,这时候就要用到第三个模板,用一个仿函数来控制使用pair的first。
那么set只有一个key,怎么写这个仿函数?这个直接返回key就可以。set传一个key的参数,最后返回一个key,map传一个pair的参数,最后返回pair的first,这样map和set都控制到了,并且都只用了RBTree一套代码来封装。
那么相应RBTree的节点,不能像上面一样用pari,因为set不适配。模板参数只改成一个T,节点数据类型为T类型。
【节点改变】
template
struct RBTreeNode
{
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
Color _col;
T _data;
RBTreeNode(const T& data)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED)
, _data(data)
{}
};
【封装set】 :Myset.h文件
#pragma once
#include "RBTree.h"
namespace wjy
{
template
class set
{
public:
struct SetKeyOfT//仿函数
{
const K& operator()(const K& k)
{
return k;
}
};
bool Insert(const K& key)
{
return _t.Insert(key);
}
private:
RBTree _t;
};
}
【封装map】:Mymap.h
#pragma once
#include "RBTree.h"
namespace wjy
{
template
class map
{
public:
struct MapKeyOfT
{
const K& operator()(const pair& kv)
{
return kv.first;
}
};
bool Insert(const pair& kv)
{
return _t.Insert(kv);
}
private:
RBTree,MapKeyOfT> _t;//树
};
}
【插入实现的改变】
//set RBTree
//map RBTree>
template
class RBTree
{
typedef RBTreeNode Node;
public:
RBTree()
:_root(nullptr)
{}
//插入的是data数据,需要比较cur->_data和data大小
//map可以直接比较,因为map只的data类型是key,但是map的data类型是pair,pair的比较
//但是pair默认的比较,first和second有一个小,那么都是小,但是我们指向用first的key来比较
//所以我们用一个仿函数KeyOfT
bool Insert(const T& data)
{
if (_root == nullptr)
{
_root = new Node(data);//构造节点变成data
_root->_col = BLACK;
return true;
}
KeyOfT kot;//将数据用kot来控制
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (kot(data)_data))
{
parent = cur;
cur = cur->_left;
}
else if (kot(data)>kot(cur->_data))
{
parent = cur;
cur = cur->_right;
}
else
return false;
}
cur = new Node(data);
cur->_col = RED;//新增节点
if (kot(data)_data))
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//控制平衡
while (parent && parent->_col == RED)//循环条件:parent存在且parent为红
{
Node* grandfather = parent->_parent;
//grandfather的左边
if (parent == grandfather->_left)//左树高
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)//叔叔存在且为红,左树和右树都一样
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//叔叔不存在 或 叔叔为黑,需要关心左树高还是右树高,因为要旋转
{
if (cur == parent->_left)//单旋
{
RotateR(grandfather);//旋转
parent->_col = BLACK;
grandfather->_col = RED;
}
else//cur==parent->_right//双旋
{
RotateL(parent);
RotateR(grandfather);
//改变颜色
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
//grandfather的右边
else//parent=grandfather->_right;
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//叔叔不存在||为黑
{
if (cur == parent->_right)//单旋
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else//cur=parent->_left--RotateRL--双旋
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
private:
Node* _root;
};
stl源码中的迭代器通过封装一个struct迭代器类,再对其进行操作。迭代器模板我们给出三个,T是数据类型,因为迭代器有普通迭代器和const迭代器,两个版本的迭代器只有数据类型不一样。返回值是T,const迭代器就是const T,返回值是迭代器的指针或引用(iterator
迭代器本质上就是个指针,所以在迭代器的struct类中传一个指针构造一个迭代器。
迭代器需要实现operator*:返回节点的数据,返回值为引用类型Ref,普通迭代器返回引用,即可以对这个数据进行读,也可以对这个数据进行写操作。operator->:返回数据的地址,所以得到的是指针指向的数据的地址,返回类型是一个指针类型Ptr。
template
struct RBTreeIterator
{
typedef RBTreeNode Node;
Node* _node;
RBTreeIterator(Node* x)
:_node(x)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
};
那么这样就可以写一个简易的迭代器,在RBTree中,将这个迭代器的begin和end写出来。begin就是二叉树的最左节点,end是最后一个节点的后面一个节点,也就是空。在stl中,红黑树实际上有一个哨兵位的头节点,这个头节点的_left指向二叉树的最左节点,也就是begin的位置,哨兵位的_right指向二叉树的最右节点,哨兵位的_parent指向二叉树的根,二叉树的根指向哨兵位头节点。这样迭代器的end就是哨兵位,也是为空的,但是这里我们自己实现就不加哨兵位头节点了。
template
class RBTree
{
typedef RBTreeNode Node;
public:
typedef RBTreeIterator iterator;
iterator begin()
{
Node* left=_root;
while (left->_left)
{
left = left->_left;
}
return iterator(left);
}
iterator end()
{
return iterator(nullptr);
}
};
以上的迭代器写法和链表没有区别,迭代器中比较有挑战的是operator++和operator--,链表中的operator++直接找到next节点即可。但是搜索二叉树的++并不是单纯的_left/_right/parent节点,而是通过中序遍历,找到下一个节点(左根右)。
在上面这颗树中,当迭代器走到13,下一个节点要走到该节点的右子树的最左节点,也就是15。所以如果当前节点的右不为空,就访问右树的最左节点。如果当前节点的右子树是空,说明该树已经遍历完了,要回溯到该节点的父节点。用15来距离,13遍历完走到15,15的右树是空,说明15这棵树走完了,那么要回溯到15的父节点。如果当前节点是父节点的左子树,那么下一个要遍历的节点就是当前节点15的父亲17;如果当前节点是父节点的右子树,那么这颗子树遍历完了,要回溯到父亲的父亲,直到找到某个根节点是父亲节点的左子树节点,如11节点,右树是空,那么找到该节点的父亲,因为8这颗子树遍历完成,8是13的左,所以下一个要遍历的节点是13。如果一直回溯直到父亲为空,说明整个红黑树遍历完了。如27这个节点,cur==27,parent==25,27是25的right,我们要找到是parent的左的节点,但是一直循环回溯,cur==25,parent==17,25是17的右;再次回溯,cur==17,parent==13,17是13的右;再次回溯,cur==13,parent==nullptr,直到父亲为空,证明这颗红黑树遍历完。over
这时三叉链的树可以走的。二叉做不到,因为没有_parent链路。
总结来说:
- 如果右子树不为空,中序下一个遍历右子树的最左节点
- 如果右子树是空,我所在的子树访问完了,沿着到根的路径,找孩子是父亲左的那个祖先。
typedef RBTreeIterator Self;
Self operator++()//++it,前置++,返回类型是迭代器类型
{
if (_node->_right)//右不为空
{
//右子树的最左节点
Node* min = _node->_right;
while (min->_left)
{
min = min->_left;
}
_node = min;
}
else//右为空,找根节点cur是parent左的节点,下一个节点就是这个parent节点,如果没有下一个节点就是nullptr,红黑树遍历完
{
Node* cur = _node;
Node* parent = _node->_parent;
while(parent && cur == parent->_right)//如果是当前节点是父节点右,继续回溯
{
cur = cur->_parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
有了operator++,当然也有operator--。operator++和--是相反的,但是大体思路是差不多的。
--遍历红黑树,就是右根左,所以如果有左子树,就找左子树的最右节点,也就是左子树的最大节点。如果没有左子树,证明这颗子树遍历完,要走右根左的根,就需要找到根是父亲右子树的这个父亲,那么这个父亲就是要找的下一个节点。
Self operator--()
{
//左边有,访问左子树最右节点
if (_node->_left)
{
Node* max = _left;
while (max->_right)
{
max = max->_right;
}
_node = max;
}
else//如果没有左子树,这颗子树遍历完,(右根左)右遍历完,要走根,找到节点是 父亲的右的父亲节点
{
Node* cur = _root;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = cur->_parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
迭代器的使用还要写一个operator!=,传给一个迭代器的引用,也就是传一个节点类型,这样,只需要比当前节点this指向的_node,和迭代器传给的对象的_node即可。
bool operator!=(const Self& s)
{
return _node != s._node;
}
set中将RBTree的迭代器封装,不是对类模板typedef,而是对类模板里面的类型进行typedef。类模板在这里还没有实例化,用模板实例化再重定义名字,需要加typename关键字。
Myset.h:
tyepdef这行代码要写在仿函数下面,因为要传模板SetKeyOfT.
typedef typename RBTree::iterator iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
Mymap.h:
typedef typename RBTree, MapKeyOfT>::iterator iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
有的人可能有疑问,为什么RBTree的实现要传3个模板参数,有了第二个模板参数T,已经能取到T中的first或key,为什么还要第一个模板参数,单独取出来呢?因为有的时候需要用第一个模板参数key的,KeyOfT仿函数能把对象中的first取出来,但是不能将类型中first取出来,就比如果你直到T类型是pair,但是还没有实例化,并不知道first是什么类型。所以还是会用到模板参数K的。find函数就会用到模板参数K。
RBTree.h的迭代器:
实例化模板仿函数,用key和kot取出来的节点的key比较,如果找到了,就返回当前节点的迭代器,找到最后到空都没有找到,就返回空的迭代器,因为end就是空的迭代器,所以直接返回end就可以。
class RBTree{
public:
typedef RBTreeIterator iterator;
iterator Find(const K& key)
{
Node* cur = _root;
KeyOfT kot;
while (cur)
{
if (key > kot(cur->_data))
{
cur = cur->_right;
}
else if (key < kot(cur->_data))
{
cur = cur->_left;
}
else
{
return iterator(cur);
}
}
return end();
}
};
Mymap.h:
class map{
public:
iterator find(const K& key)
{
return _t.Find(key);
}
};
Myset.h:
class set{
public:
iterator find(const K& key)
{
return _t.Find(key);
}
};
这里我们可以看到map和set就是空架子,它封装的就是红黑树RBTree.h的迭代器。把底层的细节都屏蔽掉了,意义就是封装。
那么map和set是不是适配器呢?我们直到map和set封装的都是红黑树,以前我们学的stack/queue/priority_queue它们都是封装的双端队列deque/vector等等,它们都是适配器(配接器),因为stack这些它既可以用deque又可以用vector或string这些等等,它可以适配任何一个,它可以封装改变。但是map和set只是单纯的封装,map/set的底层就是红黑树,不能改变,所以map/set不是适配器。
上面我们写的insert返回值类型都是bool,而在文档中它其中之一的返回值类型是pair,所以我们来改变一下红黑树的insert。
红黑树insert的改变,这里我们只改变返回值。如果这棵树是空的时候,返回键值对,键值对的first是根节点的迭代器,second是true,因为插入成功。
当插入的地方不是根,需要将插入的新增节点cur保存下来,因为如果有叔叔是红的情况,需要更新各个节点的颜色,如果更新之后还遇到两个红色节点,cur会更新成grandfather,最后cur不是新增节点。所以提前将新增节点用newnode保存起来。最后插入成功,返回的键值对first是newnode的迭代器,second是true。
pair Insert(const T& data)
{
if (_root == nullptr)
{
_root = new Node(data);//构造节点变成data
_root->_col = BLACK;
return make_pair(iterator(_root),true);
}
KeyOfT kot;
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (kot(data)_data))
{
parent = cur;
cur = cur->_left;
}
else if (kot(data)>kot(cur->_data))
{
parent = cur;
cur = cur->_right;
}
else
return make_pair(iterator(cur),false);
}
cur = new Node(data);
//保存新增节点
Node* newnode = cur;
cur->_col = RED;//新增节点
if (kot(data)_data))
{
parent->_left = cur;
cur->_parent = parent;
}
else
{
parent->_right = cur;
cur->_parent = parent;
}
//控制平衡
while (parent && parent->_col == RED)//循环条件:parent存在且parent为红
{
Node* grandfather = parent->_parent;
//grandfather的左边
if (parent == grandfather->_left)//左树高
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)//叔叔存在且为红,左树和右树都一样
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//叔叔不存在 或 叔叔为黑,需要关心左树高还是右树高,因为要旋转
{
if (cur == parent->_left)//单旋
{
RotateR(grandfather);//旋转
parent->_col = BLACK;
grandfather->_col = RED;
}
else//cur==parent->_right//双旋
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else//parent=grandfather->_right;
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//叔叔不存在||为黑
{
if (cur == parent->_right)//单旋
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else//cur=parent->_left--RotateRL--双旋
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return make_pair(iterator(newnode),true);
}
MyMap.h
如果没有key这个元素,operator[]充当的是插入,插入这个新元素key,插入的value给一个V的缺省值,最后ret是一个键值对,它的first是一个迭代器,迭代器也是一个指针,指向data元素,data在map中是一个键值对,最后返回键值对的second,也就是value值。因为返回值类型是value的引用,说明可以对value值进行修改。
V& operator[](cnost K& key)
{
pair ret = _t.Insert(key, V());
return ret.first->second;
}
所以operator[]即可以新增插入,又可以修改原来的key对应的value值。
void test_map2()
{
map dict;
dict.Insert(make_pair("sort", "排序"));
dict.Insert(make_pair("string", "字符串"));
dict.Insert(make_pair("map", "地图"));
//新增
dict["left"];
dict["left"] = "左边";
dict["map"] = "地图,映射";
//map::iterator it
auto it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
}
在Myeset.h中,拷贝构造发现没有问题,这是因为我们还没有写自定义的拷贝构造,是一个浅拷贝,set的成员变量是一个自定义类型,它会去调用RBTree的拷贝构造,RBTree的拷贝构造我们也没写,默认生成的内置类型完成值拷贝和浅拷贝。
所以我们发现,set的测试中,copy对象和s对象指向的地址是同一块空间,root的指针是一样的。就是因为完成了值拷贝和浅拷贝。
RBTree.h析构函数
但是按理来说,指向同一块空间,析构后会崩溃的,这是因为我们没有自己写析构函数,析构了两次才会崩溃。所以我们在RBTree中增加析构函数,因为析构需要用到递归,不能直接在~RBTree上写,所以需要写一个Destory函数,~RBTree调用这个Destory函数。Destory的具体实现是通过后序遍历,也就是先析构左子树,然后析构右子树,最后将根节点置空。
这里最好将Destory变成私有函数,这样保持封装性,不会被随意访问。
public:
~RBTree()
{
Destory(_root);
_root = nullptr;
}
private:
void Destory(Node* root)
{
if (root == nullptr)
{
return;
}
Destory(root->_left);
Destory(root->_right);
delete root;
}
这时候再对set的测试代码进行测试,发现崩溃了
RBTree.h拷贝构造
因为拷贝构造也要通过递归实现,所以写一个子函数copy。
通过前序遍历,一个节点的成员变量包括三叉链、颜色和data数据。所以将当前节点拷贝过来后,颜色也要拷贝,在拷贝了左子树和右子树后,父亲也要重新连接。这时候连接父亲的时候不能再通过拷贝,这样新的树就会连接到旧的树的节点上。所以如果左右子树不是空,那么左右子树的父亲就是当前节点。
public:
RBTree(const RBTree& t)//用一棵树来拷贝构造
{
_root=this->copy(t._root);
}
private:
Node* copy(Node* root)
{
if (root == nullptr)
return;
Node* newnode = new Node(root->_data);
newnode->_col = root->_col;
root->_left = copy(root->_left);
root->_right = copy(root->_right);
if (root->_left)
root->_left->_parent = newnode;
if (root->_right)
root->_right->_parent = newnode;
return newnode;
}
这里的拷贝构造不能通过节点的拷贝构造,也就是在RBTreeNode里面进行拷贝构造,因为RBTreeNode拷贝的父亲,只能将原树的父亲地址拷贝过来也就造成了新子树的父节点指向旧树的节点,所以我们还是要通过在RBTree中通过子函数实现拷贝构造。我们可以来测试一下,RBTreeNode的拷贝构造因为是内置类型,可以不用自己实现。
赋值重载,传入参数,会建立一个栈帧,这个栈帧是一个新地址,已经拷贝构造了原来对象,直接交换新对象和形参的地址,新对象原来的地址已经变成形参的地址,形参变成了新对象的地址,调用完operator=这个函数,出了作用域会直接释放形参栈帧,直接省去我们自己释放的步骤。
通过这个也告诉我们,平时我们写代码要使用引用,如果是int类型还好,如果是list或set这种,需要一个一个赋值,引用就非常方便。
RBTree& operator=(RBTree t)
{
swap(_root, t._root);
return *this;
}