map 是一个 kv 结构, set 是 k结构。
我们前面模拟实现了 红黑树,但是我们实现的红黑树把 kv 结构写死了,怎么样才能用泛型编程的思想来实现map和set呢
我们先简单看一下原码中是怎么实现的
1.原码实现逻辑
我们打开这里的 stl_set.h
通过我们前面学习红黑树,我们看到这里的 rb_tree 的名字 就是一棵纯正的红黑树
我们打开 tree.h 的头文件去找这个 rb_tree
这里用 true 和 false 来表示 red 和 black,和我们的枚举有区别,但是没影响,只要能判断红黑即可。
这里map 和 set 都使用了红黑树的代码
接下来开始我们的重头戏
我们实现节点是一个 struct 就实现了,但是这里还用到了继承,并且我们的红黑树传入了两个模版参数 K, V ,但是这里的节点却只有一个 Value 的模版参数,我们接着往下看
我们看到,源代码实现的红黑树传入参数有点多,而且下这个整体的封装过程挺麻烦的。
首先,他的 __rb_tree_node 只传入了一个 Value 的模版参数。但是 map 是 kv 模型,只传入了一个 Value ,怎么能让 map 满足两个类型呢?
我们打开 stl_set.h ,虽然这里重命名了,但是 key_type 和 value_type 的原本类型都是 Key 类型
我们把这里联系起来
那这样我们猜测,map 中应该也有一个 key_type 和 value_type ,且这两个类型不同
这里我们能看到,key_type 和 value_type 的原本类型不同,且 value_type 还是用 pair<const Key, T> 来实现的
我们先简单理解一下,这里通过一个模版参数 value_type 来确定是哪种类型,如果传入的只是一个 value_type,就是 k 模型,如果传入的参数是键值对 pair<const Key, T>,那这里就是 kv 模型
这里就通过传入参数,使 map 和 set 可以使用同一段代码,实现了泛型编程的思想
2. 模拟实现
set
namespace xsz1
{
template<class K>
class set
{
private:
RBTree<K, K> _t;
}
}
map
namespace xsz2
{
template<class K, class T>
class map
{
private:
RBTree<K, pair<K, T>> _t;
}
}
这里我们先把 K 设置为可修改的。
我们这里的写法比源代码简化了很多,因为只是模拟实现,不是完全实现,所以只是实现基本功能。
上面的 map 和 set 写完后,我们还需要修改 红黑树的基本代码
template<class T>
struct RBTreeNode
{
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
//pair<K, V> _kv;
Colour _col;
T _data;
RBTreeNode(const T& data)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_data(data)
,_col(RED)
{}
};
template<class K, class T>
class RBTree
{
public:
typedef RBTreeNode<T> Node;
//...
};
2.1. insert
但是还有一个问题需要处理,第二参数怎么比较大小?
如果是 set 比较大小很简单,都是 key,直接比较,那么 map 的键值对呢
我们看到,键值对是可以比较的,但是这个比较大小和我们想要的不一样,这里的比较大小,会先去比较 first,在 first 相同的情况下,会去比较 second,但是我们只需要比较 first,所以这里我们还需要单独处理一下
但是要怎么处理才能让 set 和 map 都适合这种用法呢
while(cur)
{
if(cur->_data <data)
{
parent = cur;
cur = cur->-right;
}
else if(cur->_data >data)
{
parent = cur;
cur = cur->_left;
}
else
{
return false
}
}
我们能不能用类型对比的方式?
if(cur->_data.type == K)
{
//...
}
如果 data 的类型和传入的第一模版参数类型相同,我们就直接比,如果传入的类型是 pair 类型,我们再对比 data.first
但是这样写肯定不行,其实 c 语言中有一种写法能支持这种思路
typeid.name()
if(typeif(cur->_data).name == K)
{
//...
}
typeif.name 可以把传入的内容转化成字符串,可以按照这个思路写下去,但是这样写不太符合我们的泛型编程的思想。
还有一个方法
template<class K, class T>
class map
{
public:
struct MapKeyofT
{
const K& operator()(const K& key)
{
return key.first;
}
};
private:
RBTree<K, pair<K, T>, MapKeyofT> _t;
};
template<class K>
class set
{
public:
struct SetKeyofT
{
const K& operator()(const K& key)
{
return key;
}
};
private:
RBTree<K, K, SetKeyofT> _t;
template<class K, class T, class KeyofT>
class RBTree
{
//....
KeyofT kot;
kot(cur->_data <kot < kot(data));
}
我们在 set 和 map 中实现两个仿函数,这两个仿函数在调用时会返回该对象的 value 。
如果是 set 调用,会返回 key,如果是 map 调用,会返回 键值对中的 first。
我们把这两个类作为模版参数传入 RBTree 中,在 RBTree 中,我们只需要去调用这个 kot 即可,kot 会把set , map 中需要比较的部分返回供我们比较,也就不需要我们自己去考虑怎么处理了。
2.2. 迭代器
2.2.1. 红黑树迭代器
红黑树,树形结构,和之前学的链表的迭代器很像。
template<class T, class KeyofT>
struct __TreeIterator
{
typedef RBTreeNode<T> Node;
typedef __TreeIterator<T, KeyofT> Self;
Node* _node;
__TreeIterator(Node* node)
:_node(node)
{}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
}
我们先把最基础的部分写出来。
上面这些都没什么问题,迭代器支持 ++,那么我们的迭代器怎么写才能支持++呢?
拿这棵树研究研究
一般情况下,会从 begin() 到 end()
begin() 的位置很好确认,就是 _root 的最左节点
观察后,我们发现,如果当前节点不存在孩子节点时,会去找节点是父亲左节点的父亲节点节点。
比如当 it== 5 或者 it == 10 时,应该会去找 节点是父亲节点左孩子节点的那个父亲节点
比如这里如果 it 是 9 ,先去找 10 ,然后走到10 的右孩子节点,此时要向上走,10这棵树已经走完了,那么就去找 10 的父节点。it 在 10 的右孩子节点时,不满足 节点是父亲左节点的条件,接着往上找,10 是 11 的父亲节点的左节点,所以 _node = 11,开始下一步。
如果节点的右子树不为空,那就去右子树中找最左节点。
这句比较好理解,和 begin() 位置一样,直接找最左节点。
我们在实现 ++ 前,先把 begin() 和 end() 实现了
iterator begin()
{
Node* cur = _root;
while(cur && cur->_left)
{
cur = cur->_left;
}
return iterator(cur);
}
iterator end()
{
return iterator(nullptr);
}
这里的 begin 和 end 函数都比较好理解
begin 最开始要指向最小的节点,所以最开始就是 cur 找最左节点即可
但是 end 要注意,并不是指向最右的节点,按照我们之前对vector 那些迭代器的理解,end 返回的是最后一个节点后面位置的位置,但是当遍历到最大节点时,树中是没有下一个节点的,所以我们默认直接返回 nullptr(按照上面++ 的逻辑,最后会指向 _root 的父节点,但是 _root 本身就是空节点,所以直接用 nullptr 返回即可)。
下面开始实现 ++ 的操作
Self& operator++()
{
if(_node->_right)
{
Node* cur = _node->_right;
while(cur && cur->_left)
{
cur = cur->_left;
}
_node = cur;
}
else
{}
return *this;
}
右不为空的情况下,去找右子树的最左节点
右为空的情况下,去找节点是父节左的那个父亲节点
Self& operator++()
{
if(_node->_right)
{
Node* cur = _node->_right;
while(cur && cur->_left)
{
cur = cur->_left;
}
_node = cur;
}
else
{
Node* cur = _node;
Node* parent = cur->_parent;
while(parent && cur == parent->_right)
{
cur = parent;
parent = cur->_parent;
}
_node = parent;
}
return *this;
}
这样,我们的 ++ 逻辑就写完了
接下来是 != 操作
bool operator!=(const Self& s)
{
return _node != s._node;
}
这里注意是节点指针进行比较,不是对比节点的值,或者是传入的 s 对象。
减减操作和加加操作相反。
++:
右树存在找右树最左节点
右树不存在,找节点是父亲左的那个父亲节点
–:
可以把情况想的极端一点,++是从最左节点遍历到最右节点,–是从最右节点遍历到最左节点,也就是说这俩操作是个完全对称的。
左树存在找最右节点
左树不存在,找节点是父亲右的那个父亲节点
Self& operator--()
{
if (_node->_left)
{
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 = cur->_parent;
}
_node = parent;
}
return *this;
}
2.2.2. set,map封装迭代器
红黑树的迭代器写完了,接下来要在 set 中也实现迭代器,因为我们要通过 set 的接口去使用红黑树。
template<class K>
class set
{
public:
//..
typedef RBTree<K, K, SetKeyofT>::iterataor iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end()
}
private:
RBTree<K, K, SetKeyofT> _t;
}
既然迭代器写好了,那么我们来试试吧
void test_set()
{
xsz1::set<int> s;
s.insert(4);
s.insert(1);
s.insert(2);
s.insert(3);
s.insert(2);
xsz1::set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
当我们运行这段代码后,我们会惊奇的发现
他崩了
注意这里显示出来的问题,iterator 不是类型,我们明明 typedef 了 红黑树的iterator, 为什么这里说 iterator 不是类型呢?
这里要注意,域作用访问限定符。
typedef RBTree<K, K, SetKeyofT>::iterataor iterator;
一般除了这里会用到 这个操作符,我们在声明 其类中的静态成员变量也会用到这个操作符,因为我们的红黑树代码是用模版来实现的 ,所以这段代码在预编译的时候并不存在红黑树的代码(实例化后才会出现,想要实例化也是在运行期间),而预编译时访问到了这段代码,编译器就不知道你是想给 iterator 这个静态成员变量重命名,还是给这个类型命重命名,所以会报错。
因此如果想通过这里,我们必须强制说明一下
typedef typename RBTree<K, K, SetKeyofT>::iterataor iterator;
现在可以开测我们的代码了
我们看到,通过迭代器,随机插入的数据正向输出了。
迭代器实现了,范围 for 也就随便用。
然后是 map 的迭代器,与 set 同理
template<class K>
class set
{
public:
//..
typedef RBTree<K, pair<K, T>, MapKeyofT>::iterataor iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end()
}
private:
RBTree<K, pair<K, T>, MapKeyofT> _t;
}
我们运行之后
完全没什么问题。
虽然 set,map 大题功能已经实现,但不是还存在一个很严重的问题。
如果我们去修改 迭代器位置的值
(*it) = 100;
我们看到,修改后的数据并不会自动去调整,而且最小的值被修改成了 100,我们的红黑树结构也遭到了破坏。
所以接下来我们要想办法,如何修改才能使key 值不可修改。
2.2.3. key 值不可修改
想要解决这个问题,我们先去库里看看大佬的思路。
set.h
set 只含有一个 key,在这里 iterator 被处理为 const_iterator,我们去看看 map 是怎么处理的
map并的处理方式和 set 完全不一样。
set 只含有一个值,这个key值本身就不能被修改,所以这里的 const_iterator 就能保证其中的 key 值不会被修改。
但是 map 中含有 key 和 value,map 中的 key 值不能被修改,但是 value 值是可以被修改的,所以 map 就不能那么暴力的直接全部修改为 const_iterator。
这里 set 和 map 的实现逻辑就不太一样了。
首先,我们先把insert 的返回值修改一下,库里面的返回值是键值对,这里我们也返回键值对
pair<iterator, bool> insert(const T& data)
{
//...
}
红黑树的 insert 修改后,要注意 map.h 和 set.h 中的 insert 的返回值也要修改。
pair<iterator, bool> insert(const T& data)
{
return _t.insert(data);
}
因为 set 需要用到const_iterator 所以我们先在红黑树中把 const_iterator 实现了
这里和我们前面实现 list 的const_iterator 的做法一样,我们在迭代器的模版参数中传入 Ref(reference参考值) 和 Ptr(指针), 当我们调用 const_iterator 时,就给 迭代器传入 const T& 和 const T*,如果我们要用 iterator, 我们就传入 T& 和 T*
template<class T, class Ref, class Ptr, class KeyofT>
struct __TreeIterator
{
//...
}
template<class K, class T, class KeyofT>
class RBTree
{
public:
typedef __TreeIterator<T, T&, T*, KeyofT> iterator;
typedef __TreeIterator<T, const T&, const T*, KeyofT> const_iterator;
//...
}
const_iteraor 写好后,我们就开始操作 set
typedef typename RBTree<K, K, SetKeyofT>::const_iterator iterator ;
typedef typename RBTree<K, K, SetKeyofT>::const_iterator iterator;
pair<iterator, bool> insert(const K& key)
{
return _t.insert(key);
}
iterator begin()const
{
return _t.begin();
}
iterator end()const
{
return _t.end()
}
把这里写好之后,我们尝试修改 set 中的值
这个时候,这里就会报错,我们不拿这里处理之后,运行代码,我们发现
还是报错了。
这里的问题就比较复杂了,
先看我们 红黑树 和 set 中 insert 中的返回值
pair<iterator, bool> insert(const T& data)
{
return make_pair(iterator(cur), true);
}
//set.h
pair<iterator , bool> insert(const K& key)
{
return _t.insert(key);
}
这里和 pair 的特性相关
我们知道,pair 的牛逼之处在于它的拷贝,他不需要我们一定要传入相同类型的值才能拷贝构造,而是只要 传入参数 pair 中的 第一第二参数能构造 被传入 的 pair 中的第一第二参数,就能完成 pair 的拷贝构造
void func(pair<string, const int> p1)
{
//..
}
int main()
{
pair<const char*, int> p;
func(p);
return 0;
}
这里是没有问题的,因为 const char* 能构造 string, int 能构造 const int,所以 p 就能去构造 p1。
现在回到上面的问题,为什么这里会报错。
我们先清楚一件事,set 的 insert 返回值pair 中的类型iterator,对红黑树来说是 const_iterator 类型。
红黑树返回的类型是 pair<iterator, bool> 类型,
这里的 iterator 构造出来的类型 是 __TreeIterator<T, Ref, Ptr, KeyofT> 的类型
但是我们set 接收到这个类型后·,再返回 pair<const_iterator, bool> 类,型这里的 返回类型 是
RBTree<K, K, SetKeyofT>::const_iterator 类型,也就是
__TreeIterator<K, T, const T&, const T*, KeyofT>。
红黑树的 insert 返回的 pair 中 的迭代器没有 const T 的类型,但是 从 set 的 insert 返回的 pair 的迭代器中含有 const T 类型,pair 能做到 模版参数只要参数类型能构造就行,但是 我们模拟实现的迭代器是不支持自我完善的,也就是说 __TreeIterator<T, Ref, Ptr, KeyofT> 不能去构造 __TreeIterator<K, T, const T&, const T*, KeyofT> 类型 。
解决方法
所以我们可以考虑,不要使用它的默认拷贝构造,我们自己去写一个拷贝构造函数
除了这个方法,还有个更简单的方法
既然 pair 不需要传入和目标相同的模版参数,只要类型能构造就行,那么我们管目标是什么迭代器,我们直接把节点给set,set拿到节点爱怎么构造不关我红黑树的事
pair<Node*, bool> insert(const T& datd)
{
//...
return make_pair(cur, true);
//...
}
此时我们的代码就能正常运行了,set 拿到节点,就要去自己用节点去构造 const_iterator。
这里要注意 const_iterator 和 const iterator 不是一个东西,const iterator只能保证这个迭代器不能修改,也就不能进行++,–等操作, 但是是可以修改 iterator 内的值。而 const_iterator 是我们自己实现的可以进行 ++,–等操作的类,只是这个类中的成员都用 const 进行修饰,成员不能修改。
然后就是 map 了,map 因为迭代器不那样暴力处理,我们想既然 key 不能修改,但是 value 可以修改,那么把 所有返回和 K 相关的地方,都返回 const K 应该就行了吧。
template<class K, class T>
class map
{
public:
struct MapKeyofT
{
const K& operator()(const pair<K, T>& key)
{
return key.first;
}
};
typedef typename RBTree<K, pair<const K, T>, MapKeyofT>::iterator iterator;
typedef typename RBTree<const K, const pair<const K, T>, MapKeyofT>::iterator const_iterator;
pair<iterator, bool> insert(const pair<const K, T>& key)
{
return _t.insert(key);
}
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
T& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, T()));
return (ret.first)->second;
}
private:
RBTree<K, pair<const K, T>, MapKeyofT> _t;
};
大概逻辑写完后,我们看到 map 中的 第一参数不能修改,第二参数可以修改。
2.2.4. operator[]
既然我们上面把迭代器整体都玩完了,那么再写一个 [] 操作助助兴。
这里的解析在 map_set 学习的那节,想要了解这个怎么操作的可以去那里了解,这里主要进行实现
T& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, T());
return (ret.first)->second;
}
注意这个操作要完成的事,如果 [] 位置不存在值则是插入,如果存在值则返回其中的 value
void test_map2()
{
string arr[] = { "香蕉", "甜瓜", "苹果", "西瓜","香蕉", "甜瓜", "苹果", "西瓜", "香蕉", "甜瓜", "苹果", "西瓜", "香蕉", "甜瓜", "苹果", "西瓜", "香蕉", "甜瓜", "苹果", "西瓜" };
xsz2::map<string, int> m;
for (auto& e : arr)
{
m[e]++;;
}
for (auto& e : m)
{
cout << e.first << " " << e.second << " ";
}
cout << endl;
}
这样我们的 [] 操作节完成了。
最后一个问题,我们在 RBTree 中传入了 模版参数 K,但是我们这里全程没用到过这个类型,我们需要 key 时直接使用了 KeyofT,这个 K 模版参数真的有必要吗?
有,因为我们这里主要实现的是 insert 操作,set,map中含有其他操作,比如 find,如果要实现find 操作
iterator find(const k& key)
{
//...
}
可以说我们在简单的模拟实现这里没用上,但是绝对不能说他没有用。