【C++】用一棵红黑树同时封装出map和set
苦厄难夺凌云志,不死终有出头日。
文章目录
- 一、封装第一层:仿函数取结点中的key关键码
- 二、封装第二层:红黑树的普通迭代器
- 1.map和set的表层迭代器实现
- 2.底层红黑树中迭代器的实现
- 三、封装第三层:
- 1.set的迭代器(底层均为const_iterator)
- 2.map的const_iterator(键值对中的Key类型写死为const修饰的类型,则定义出来的都是常量,不能被修改)
- 3.map的[ ]运算符重载
- 4.Insert返回的iterator和set表层的const_iterator返回类型不兼容(重写迭代器的拷贝构造函数)
- 四、对于红黑树设计产生的问题
一、封装第一层:仿函数取结点中的key关键码
1.
在源码里面,对于map和set的实现,底层是用同一棵红黑树封装出来的,并不是用了两棵红黑树,一个红黑树结点存key,一个红黑树结点存<key,value>的键值对,这样的方式太不符合大佬的水准了,实际上在红黑树底层中用了一个模板参数Value来代表红黑树结点存储对象的类型,这个类型可能是pair键值对,也有可能是key类型。
所以在红黑树这一层中是不知道他自己的结点要存储什么类型的对象的,故而需要模板参数来接收存储对象的类型。
2.
第一个问题,既然通过模板参数Value就能够确定红黑树实现的是map还是set了,那我还需要模板参数Key干什么啊?如果Value是键值对,那红黑树此时封装的就是map,如果Value是key,那红黑树此时封装的就是set。
对于红黑树的find()函数来讲,我们在传参的时候,find形参类型肯定得是key,此时就发挥出Key模板参数的作用了,因为在红黑树搜索的时候,依靠的还是关键码进行搜索,通过所传关键码和红黑树结点中的关键码的大小关系,确定到红黑树中要搜索的某个结点。
template <class T>
struct RBTreeNode
{
T _data;// 我们不清楚红黑树结点里面存的是什么,可能是string,内置类型,又或是键值对,取决于map和set里面存的是什么
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
enum color _col;
RBTreeNode(const T& data)
:_data(data)
,_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_col(RED)
{}
};
3.
第二个问题,红黑树在插入结点的时候,不知道结点的类型,怎么拿出结点中的关键码进行比较插入啊?如果是pair,我们应该拿pair的first进行比较,如果是key,我们直接比较即可,但在红黑树中该如何实现具体代码呢?
此时仿函数就登场了,我们在map和set中都增加一个仿函数类,在给红黑树模板传参的时候,除Key和T类型外(由于库里面的Key和Value容易误导大家,库里的Value类型不是键值对中的value,而是红黑树结点存储对象的类型,所以我们自己写的红黑树第二个模板参数采用的命名是T),再增加一个用于获取结点的关键码的仿函数类型,也就是KeyOfT仿函数,这个仿函数实例化出的对象的作用就是帮助红黑树完成结点的插入,方便在插入内部实现时进行结点之间关键码的比较。
template<class K, class V>
class map
{
struct MapKeyOfT
{
const K& operator()(const pair<const K, V>& kv)
{
return kv.first;
}
};
public:
private:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;//pair键值对的关键吗是常量
};
template<class K>
class set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
private:
RBTree<K, K, SetKeyOfT> _t;
};
template <class K, class T, class KeyOfT>
class RBTree
{
public:
typedef RBTreeNode<T> Node;// T代表红黑树结点的存储内容的类型
在拥有仿函数类型后,我们实例化一个仿函数对象,这个对象的功能就是帮助我们取得结点中的key关键码,方便我们在红黑树Insert的实现里面进行结点中关键码的比较
二、封装第二层:红黑树的普通迭代器
1.map和set的表层迭代器实现
1.
下面是表层的map和set的迭代器实现,注意我们没有实现map和set的const迭代器,在封装第二层这里,仅仅只实现普通迭代器,第三层封装的时候都会加上。
其实map和set的表层迭代器实现都很简单,他们都是直接调用了底层红黑树的普通迭代器,所以难点其实是在红黑树的迭代器实现上。
2.
对红黑树类型中的迭代器类型进行typedef时,可以看到我们在typedef后面加了typename,typename的作用就是告诉编译器后面的东西是一个类型,你先不要编译他,等到模板实例化为真正类型后,你再去取他里面的内嵌类型iterator。因为我们知道编译器是不编译模板的,编译的是模板实例化之后的代码,只有模板实例化之后,它里面的内嵌类型我们才可以取到,所以如果你不加typename,有可能取的不是类型,因为静态变量或函数都是可以通过类+域访问符进行访问的,所以如果你要取模板里面的类型,那就必须在模板前面加typename,告诉编译器你取的是类型。
template<class K, class V>
class map
{
struct MapKeyOfT
{
const K& operator()(const pair<const K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::iterator iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
bool insert(const pair<const K, V>& kv)
{
return _t.Insert(kv);
}
private:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
template<class K>
class set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
bool insert(const K& key)
{
return _t.Insert(key);
}
private:
RBTree<K, K, SetKeyOfT> _t;
};
2.底层红黑树中迭代器的实现
1.
红黑树的迭代器和list的迭代器实际是一个道理,两个容器的原生指针均无法满足迭代器的要求,所以道理还是相同,我们进行类封装+运算符重载,让类实例化后的对象能够满足迭代器的要求,使其行为像指针一样。
2.
稍有难度的就是++和- -的运算符重载的实现,但红黑树的三叉链结构能帮助我们省不少事,迭代器移动的本质就是让__RBTreeIterator类里面的原生指针_node指向红黑树的中序位置的下一个结点。
其实大体就分两种情况,对于1位置的it而言,由于他是begin,所以他的左边一定为空,那就需要先判断他的右边是否为空,如果不为空,那就需要先访问他的右子树的最左结点,但在图里面这个最左结点恰好就是6,我们将it对象里的_node指向更新到结点6处,然后等到it的右边为空时,我们需要判断当前结点和他的父亲结点的位置关系,如果此时结点是父亲的左,那就说明父亲结点还没有访问,因为是左子树 根 右子树的访问顺序,那就得先访问父亲,然后再去访问父亲的右,如果此时结点是父亲的右,那就说明父亲结点已经被访问过了,所以需要回溯找祖先,就比如6访问完了,6是1的右,那就回溯找1的父亲,此时如果1是8的左,那就访问8就好了,但如果1是8的右,那就还需要迭代向上找祖先,这就是++重载的大体思路,- -与其类似,这里不过多赘述。
3.
红黑树结点里面存储的是一个个自定义类型或者是内置类型定义出来的变量,由这些变量混和组成了RBTreeNode结构体,所以* 重载返回的是_data变量,→重载返回的是变量的地址,而++和- -返回的是* this,this指针其实就是结构体指针,只不过this现在指向的是一个迭代器对象,那么* this实际返回的就是迭代器对象本身。
其实这些难度都不大,对我们的要求也不高,只要list迭代器理解的透彻,那红黑树的迭代器也很好理解,唯一不同的是红黑树迭代器内部多加了一点算法而已。
template<class T>
struct __RBTreeIterator
{
typedef RBTreeNode<T> Node;
typedef __RBTreeIterator<T> Self;
Node* _node;
__RBTreeIterator(Node* node)
:_node(node)
{}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
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 && cur == parent->_right)
{
cur = cur->_parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
Self& operator--()
{
//右子树 根 左子树
if (_node->_left)
{
//找左子树的最右结点,其实就是次大的结点
_node = _node->_left;
while (_node->_right)
{
_node = _node->_right;
}
}
else
{
Node* cur = _node;
Node* parent = _node->_parent;
while (parent && cur == parent->_left)
{
cur = parent;
parent = parent->_parent;
}
//如果cur是parent的右,则直接让_node指向到parent即可
_node = parent;
}
return *this;//返回迭代器对象
}
bool operator!=(const Self& it)const//const修饰表示在该成员函数内部,不能修改对象的任何成员变量
{
return this->_node != it._node;
}
bool operator==(const Self& it)const//const对象和非const对象都可以调用
{
return this->_node == it._node;
}
};
三、封装第三层:
1.set的迭代器(底层均为const_iterator)
1.
从源码的实现我们可以看到,set表层的两个迭代器其实都是由红黑树的const迭代器typedef出来的,至于原因也很好理解,由于set的红黑树结点只存储key关键码,所以他是不允许被修改的,那其实就是迭代器指向的内容不可被修改,所以我们用set迭代器时,严格来说应该使用const_iterator,但就算你使用iterator,set也不会允许你修改iterator指向的内容,因为底层用的是红黑树的const_iterator,所以第三步封装这里,我们也给set的迭代器搞成像库里面一样。
2.
结构体SetKeyOfT放在哪无所谓,仅仅只是形式变了一下而已,当你修改set表层的迭代器之后,你一编译就会报错,编译器会产生一大堆的模板错误,其实是由于类型不兼容导致的。普通set对象调用begin()时,此时调用的是普通版本,那么底层调用的红黑树的begin()也调用的是普通版本,所以最终会返回一个普通迭代器,但表层set的Iterator实际又是红黑树的const迭代器类型,此时就会发生返回值和返回类型不兼容的问题,那应该怎么办呢?答案就是看源码。
3.
从源码中可以看到set的begin和end都用了const版本,且仅仅只有const版本的begin和end,我们知道,普通对象会优先调用普通版本的函数,但如果只有const版本的函数,那普通对象也会调用const版本的函数。由于const版本函数中只能读,不能写,所以普通对象会被const版本函数认为是const对象,那在调用底层红黑树的begin和end时,就会自动调用红黑树中的const版本的begin和end,此时返回值和返回类型就兼容了,不会产生编译错误。
4.
这里在补充一点知识,一个函数是否需要实现const版本取决于这个函数的功能是什么,如果这个函数对于容器来说仅仅是只读的功能,那就实现const版本,如果可以写可以读那就实现两个版本,如果仅仅是只写,那就实现非const版本。
template <class K>
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
template <class K>
class set
{
public:
//set表层的普通迭代器底层用的依旧是const_iterator,就是不允许对set的迭代器指向内容进行修改
typedef typename RBTree<K, K, SetKeyOfT<K>>::const_iterator iterator;
typedef typename RBTree<K, K, SetKeyOfT<K>>::const_iterator const_iterator;
iterator begin()//const
{
return _t.begin();
//普通对象调用的begin()返回的是普通迭代器,而set上层要求返回的都是const迭代器,发生类型不兼容问题
//库是怎么解决的呢?
//
//set的begin和end没有提供两个版本,仅仅只提供了const版本,强制性要求set的普通对象和const对象底层都去调用
//红黑树const版本的begin()和end()
//注意:如果是普通对象发生调用const函数,那么在const函数内部,普通对象也会被当作const对象进行处理。
// 所以底层调用的红黑树的begin,自动就是const版本的begin()
}
iterator end()//const
{
return _t.end();
}
5.
从下面红黑树和红黑树结点两个类的模板参数其实就可以看到list的const迭代器的影子,我们用Ref和Ptr作为红黑树迭代器的模板参数,和list迭代器非常相似,这样在迭代器内部就可以直接用参数Ref和Ptr作为→和*重载的返回值,完成const迭代器的要求,返回常引用和const修饰的指针内容。
template <class T, class Ref, class Ptr>
struct __RBTreeIterator
{
typedef RBTreeNode<T> Node;// node里面存的是键值对或key类型变量
typedef __RBTreeIterator<T, Ref, Ptr> Self;//迭代器
Ref operator*()
{
return _node->_data;// 如果是set就是key,如果是map就是键值对,这里返回引用
}
Ptr operator->()
{
return &_node->_data;
}
}
template <class K, class T, class KeyOfT>
class RBTree
{
public:
typedef RBTreeNode<T> Node;// T代表红黑树结点的存储内容的类型
typedef __RBTreeIterator<T, T&, T*> iterator;
typedef __RBTreeIterator<T, const T&, const T*> const_iterator;
iterator begin()//给普通对象调用
{
Node* left = _root;
while (left && left->_left)// 这棵树有可能是空树,若不为空树找到左树最小结点
{
left = left->_left;
}
return iterator(left);// 不能瞎用引用啊,这里的迭代器对象出了begin()作用域就销毁了,引用就出大事了!
}
const_iterator begin()const//给const对象调用
{
Node* left = _root;
while (left && left->_left)
{
left = left->_left;
}
return const_iterator(left);
}
iterator end()
{
return iterator(nullptr);
}
const_iterator end()const
{
return const_iterator(nullptr);
}
2.map的const_iterator(键值对中的Key类型写死为const修饰的类型,则定义出来的都是常量,不能被修改)
1.
map的迭代器是需要实现两个版本的,因为map容器中的数据是可以被修改的,如果你想修改map中的数据那就用iterator,如果你想修改map中的数据那就用const_iterator。
如果是const_iterator,那解引用或者→返回的就是键值对的常引用或const修饰的指向键值对的结构体指针,那么此时键值对的key和value都是不可以修改的。
如果是iterator,解引用或者→返回的就是键值对的普通引用或无const修饰的指向键值对的结构体指针,但此时键值对的key依旧不可以被修改,只能对键值对中的value进行修改,因为在给红黑树模板传参的时候,键值对中的K我们写死了,用的就是const修饰的K,那么在键值对结构体中K类型定义出来的变量或对象就是常量,因为const修饰的任何变量都可以称为常量,所以即使你用的是iterator,他所指向的键值对的key依旧是不能修改的,我们只能修改他的value。
2.
对于const关键字,const修饰的变量我们称之为常量,const修饰的对象我们称之为常对象,常量不能被修改,常对象的成员变量也不能被修改。
template <class K, class V>
class map
{
public:
// 1.取未实例化类模板里面的类型时,无论是内部类还是内嵌类型,都需要加typename,告诉编译器类模板后面的东西是类型
// 因为类模板里面还有可能是静态成员,静态成员也是可以通过类名+域访问符进行访问的,编译器无法确定是类型还是静态成员
// 2.图纸里面怎么能直接取东西呢?肯定是需要对这样的情况进行特殊处理的
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT<K,V>>::iterator iterator;
// 编译器不会编译模板,编译的都是模板实例化之后的代码,所以iterator是静态成员还是内嵌类型,编译器都确定不了。
//typename就是先告诉一下编译器,这里是类型,先不要取这个类型,等到模板实例化好之后你再去取这个类型。
//图纸里面有水果,你先不要去拿这个水果,等到按照图纸实例化为苹果树之后,你再取这个苹果
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT<K, V>>::const_iterator const_iterator;
//map的迭代器需要提供两个版本
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<K, pair<const K, V>, MapKeyOfT<K,V>> _t;
};
3.map的[ ]运算符重载
1.
如果我们自己封装的map也想像库里面的[ ]重载函数一样,能够同时具有增查改的功能,我们该怎么实现呢?那就需要从红黑树的insert来入手,此时的insert返回的就不再是ture或false了,返回的应该是一个键值对,这个键值对的first是指向结点的迭代器,second是bool值。
2.
[ ]在进行键值对的insert时,value使用的是其类型的无参构造,如果是内置类型则值为0或nullptr这些,如果是自定义类型则调用其默认的构造函数。
template <class K, class V>
class map
{
public:
pair<iterator, bool> insert(const pair<const K, V>& kv)
{
return _t.Insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return (ret.first)->second;
}
private:
RBTree<K, pair<const K, V>, MapKeyOfT<K,V>> _t;
};
3.
下面是红黑树这一层Insert的实现,我们对其内部实现稍作改动,更改其返回值为键值对,如果出现插入key值相等的情况,则将bool改为false即可。最后调色后返回键值对时,我们不能直接返回cur构造的迭代器,因为如果发生调色,则cur位置会更改,所以在插入结点后,用一个curit的结点指针记录一下插入结点的位置,最后返回由curit结点指针构造出来的迭代器。
pair<iterator, bool> Insert(const T& data)
//红黑树必须通过结点里面存储的key来进行比较才可以插入结点,所以出现了kot仿函数对象
{
KeyOfT kot;
Node* parent = nullptr;
Node* cur = _root;
if (_root == nullptr)
{
_root = new Node(data);
_root->_col = BLACK;
return make_pair(iterator(_root), true);
}
while (cur)
{
if (kot(cur->_data) < kot(data))
{
parent = cur;
cur = cur->_right;
}
else if (kot(cur->_data) > kot(data))
{
parent = cur;
cur = cur->_left;
}
else
{
return make_pair(iterator(cur), false);
}
}
cur = new Node(data);
Node* curit = cur;//保存一下cur的位置否则下面旋转调色过程中,cur有可能被改了
if (kot(parent->_data) > kot(data))
{
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 (cur == parent->_left)
{
RotateR(grandparent);
grandparent->_col = RED;
parent->_col = BLACK;
break;
}
else if (cur == parent->_right)
{
RotateL(parent);
RotateR(grandparent);
cur->_col = BLACK;
grandparent->_col = RED;
break;
}
else
{
assert(false);
}
}
else
{
Node* uncle = grandparent->_left;
if (uncle && uncle->_col == RED)
{
grandparent->_col = RED;
uncle->_col = parent->_col = BLACK;
cur = grandparent;
parent = cur->_parent;
}
else if (cur == parent->_right)
{
RotateL(grandparent);
parent->_col = BLACK;
grandparent->_col = RED;
break;
}
else if (cur == parent->_left)
{
RotateR(parent);
RotateL(grandparent);
cur->_col = BLACK;
grandparent->_col = RED;
break;
}
else
{
assert(false);
}
}
}
_root->_col = BLACK;
return make_pair(iterator(curit), true);
}
4.Insert返回的iterator和set表层的const_iterator返回类型不兼容(重写迭代器的拷贝构造函数)
1.
我们知道一般情况下,迭代器的拷贝构造是不需要我们自己写的,因为编译器默认生成的值拷贝就够用了,两个迭代器之间进行结点指针的赋值即可,这两个迭代器我们一般都默认为相同类型,比如普通迭代器和普通迭代器进行赋值,const迭代器和const迭代器进行赋值。
但其实库里面的容器都支持用普通迭代器去拷贝构造const迭代器,下面的list和vector都支持这样的操作,那这样的操作是怎么实现的呢?
int main()
{
list<int> lt;
vector<int> v;
list<int>::const_iterator clit = lt.begin();
vector<int>::const_iterator cvit = v.begin();
return 0;
}
2.
其实实现这样的操作并不复杂,不过我们需要重写迭代器的拷贝构造,当const迭代器调用自身拷贝构造时,我们想让拷贝对象是普通迭代器,那就不能用Self作为拷贝构造函数的形参类型,所以此时就重新在迭代器内部定义一个固定不变的类型iterator,用这个类型作为拷贝构造的形参类型。
所以当const迭代器之间进行拷贝的时候,const迭代器类里面是没有写const迭代器之间的拷贝构造的,所以编译器会默认生成拷贝构造。
当普通迭代器之间进行拷贝的时候,普通迭代器类里面写了普通迭代器之间的拷贝构造,那么编译器就不会默认生成拷贝构造。
当const迭代器被普通迭代器拷贝的时候,const迭代器类里面的构造函数会被调用,即用普通迭代器构造出const迭代器。。
template <class T, class Ref, class Ptr>
struct __RBTreeIterator
{
typedef RBTreeNode<T> Node;// node里面存的是键值对或key类型变量
typedef __RBTreeIterator<T, Ref, Ptr> Self;//迭代器
typedef __RBTreeIterator<T, T&, T*> iterator;
Node* _node;
__RBTreeIterator(Node* node)
:_node(node)
{}
//如果是const迭代器调用,这里是构造。如果是普通迭代器调用,这里是拷贝构造。
__RBTreeIterator(const iterator& it)
:_node(it._node)
{}
3.
既然set不是要返回const迭代器么,我们只需要先接收一下insert的键值对,然后再重写拷贝构造一个键值对即可,键值对的first拷贝会调用iterator类型的拷贝构造函数(我们重写的拷贝构造就派上用场了),bool值则直接进行值拷贝即可。
template <class K>
class set
{
public:
pair<iterator, bool> insert(const K& key)//这里的迭代器被替换为const_iterator
{
//return _t.Insert(key);
//Insert返回的是普通迭代器,普通迭代器不能直接拷贝构造给const迭代器,类型不同,但我们可以自己写拷贝构造。
//这里肯定不能再使用const修饰来进行解决了,因为insert功能就是要写嘛,又不具有读的功能,实现const版本干嘛???
pair<typename RBTree<K, K, SetKeyOfT<K>>::iterator, bool> ret = _t.Insert(key);
//必须用红黑树底层的普通迭代器类型接收Insert返回的键值对,因为set的所有迭代器都是底层const迭代器typedef出来的
return pair<iterator, bool>(ret.first, ret.second);
//这里在利用Insert的返回值重新拷贝构造一个键值对,用普通迭代器构造const迭代器,然后返回具有const迭代器的键值对
//********set这个地方即使自己写了拷贝构造,在返回的时候也依旧不能直接强转,这谁能想到啊????????????
}
private:
RBTree<K, K, SetKeyOfT<K>> _t;
};
四、对于红黑树设计产生的问题
1.
map底层的红黑树存的是<key,value>的键值对,set底层的红黑树存的是key关键码,我当时觉得为什么一定要设计成这样呢?我们让map的红黑树结点只存储value不可以吗?修改value不就行了吗?搞个键值对干嘛啊,其实这个问题很好解决,我当时也是蒙了产生这样的问题。
红黑树插入结点的原理是什么呢?原理不就是和搜索树一样么,只有结点之间能够进行比较我们才能将结点按照搜索树的规则进行插入啊,那如果红黑树结点想要进行比较,那只能通过结点的key来进行比较,所以map就不能只存value,必须存储键值对,只有这样才能进行结点之间的比较。