C++ Map Set的模拟实现
文章目录
- 前言
- 一、Map 和 Set是什么?
- 1.Set
- 2.Map
- 二、困难点
- 困难一、set和map中值的类型不同
- 困难二、Map和Set中值不可修改
- 困难三、红黑树中迭代器的++和--
- 1.++
- 2.- -
- 困难四、map中[ ] 运算符重载的实现
- 1.修改红黑树以及Map和Set中insert的返回值
- 1.修改set
- 2.修改Map
- 2.[ ]的重载
- 总结
前言
随着平衡二叉树和红黑树插入(旋转+变色)的实现,我开始进一步学习Map和Set的模拟实现。
一、Map 和 Set是什么?
1.Set
- set是按照一定次序存储元素的容器
- 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。
set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。 - 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行
排序。 - set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对
子集进行直接迭代。 - set在底层是用二叉搜索树(红黑树)实现的。
2.Map
- map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元
素。 - 在map中, 键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair:typedef pair<const key, T> value_type;
- 在内部,map中的元素总是按照键值key进行比较排序的。
- map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序
对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。 - map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
- map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))
二、困难点
困难一、set和map中值的类型不同
由于Set只是Key,Map是KV结构。但其底层都是用红黑树去实现的,那么我们该如何去构造这颗红黑树呢?
- 我们通过在红黑树中传三个模板参数来实现这个问题。
template<class K,class V,class Value>
这里的K为Map和Set中Key的类型,而V(真实存储值的地方)对于Map来说是一个pair类型,对于Set来说是Key,Value是一个类模板,它通过在类中重写()括号运算符来使红黑树拿到要比较的值。(Set为Key,Map也为Key)
- 对于Set来说
template <class K>
class set
{
public:
class SetOfVal
{
public:
const K& operator()(const K& data)
{
return data;
}
};
private:
RBTree<K,K,SetOfVal> _t;
};
我们通过在SetOfVal类重写括号运算符,实现在红黑树中用K进行比较。
- 对于Map来说
template <class K,class V>
class map
{
public:
class MapOfVal
{
public:
const K& operator()(const pair<const K, V>& data)
{
return data.first;
}
};
private:
RBTree< K,pair<const K,V>,MapOfVal> _t ;
};
我们通过MapOfVal类重写括号运算符,实现在红黑树中用V.first进行比较。
困难二、Map和Set中值不可修改
- 对于Map来说,其Key值是不可修改的,而Value值是可以修改的。
这里我们借鉴了Stl库,其巧妙的在构造V类型模板参数时,对key传了一个const来解决这个问题。
RBTree< K,pair<const K,V>,MapOfVal> _t ;
- 对于Set来说,其值也是不可修改的。
如果允许修改,则就不能满足它是中序有序的了。
这里通过定义迭代器来实现。
它直接将const_iterator 定义为 iterator来保证不可修改。
typedef typename RBTree<K, K, SetOfVal>::const_iterator iterator;
typedef typename RBTree<K, K, SetOfVal>::const_iterator const_iterator;
iterator begin()const
{
return _t.begin();
}
iterator end()const
{
return _t.end();
}
注意这里我们在写普通迭代器的begin,end时,需要让变量加一个const(如果不加,则它会调用红黑树中普通迭代器的begin和end,从而报错)
困难三、红黑树中迭代器的++和–
首先我们规定begin返回这棵树中 中序遍历的第一个结点。
end返回空指针。
1.++
对于一个结点来说,++就是要找它在中序遍历中的下一个结点,我们令这个结点为*p。
两种情况
- 如果这个结点有右孩子,则p = 右子树中最小的那个结点。即右子树的最左边结点。
- 如果这个结点没有右孩子,则令parent = p->parent。
如果 p为parent的左孩子,表示p这颗树已经遍历完成,则下一个应该为parent。
如果p为parent的右孩子,则表示parent这颗子树已经全部遍历完成。
令p = parent, parent = parent->parent ,继续向上遍历。(最后若根节点,则返回nullptr)
Self& operator++()
{
if (_node->_right != nullptr)
{
Node* p = _node->_right;
while (p->_left != nullptr)
p = p->_left;
_node = p;
return *this;
}
else
{
Node* parent = _node->_parent;
while (parent != nullptr)
{
if (parent->_left == _node)
{
_node = parent;
return *this;
}
else
{
_node = parent;
parent = parent->_parent;
}
}
_node = nullptr;
return *this;
}
}
Self operator++(int)
{
Self temp(_node);
++*this;
return temp;
}
2.- -
减减的逻辑和加加刚好是相反的。
去判断有没有左孩子,若有,则为左孩子的最右结点。
若没有,则取判断parent和p的关系。
在此不过多赘述。
Self& operator--()
{
if (_node->_left != nullptr)
{
Node* p = _node->_left;
while (p->_right != nullptr)
p = p->_right;
_node = p;
return *this;
}
else
{
Node* parent = _node->_parent;
while (parent != nullptr)
{
if (parent->_right == _node)
{
_node = parent;
return *this;
}
else
{
_node = parent;
parent = parent->_parent;
}
}
}
}
Self operator--(int)
{
Self temp(_node);
--*this;
return temp;
}
困难四、map中[ ] 运算符重载的实现
我们可知如何k已经存在于Map中,则返回它Value的引用。
如果不存在,我们可知需要将其先插入进去,然后返回它Value的引用。
而Insert函数的返回值是一个pair类型,其first为一个指向该元素的迭代器,second是一个bool类型,表示插入成功与否。
所以,我们需要先修改Insert的其返回值。
1.修改红黑树以及Map和Set中insert的返回值
1.修改set
pair<iterator,bool> insert(const K& data)
{
pair<typename RBTree<K, K, SetOfVal>::iterator, bool> ret = _t.Insert(data);
return pair<iterator, bool>(ret.first, ret.second);
}
这里需要注意,set中的iterator其实是一个const_iterator。
而红黑树返回的是一个正常的iterator,如果直接这样写会报错。
所以我们需要在迭代器中写一个拷贝构造函数。
其作用是
- 在我们需要正常迭代器时进行拷贝构造。
- 在我们需要const迭代器时用正常迭代器来初始化const迭代器。
typedef __RBTreeIterator<T, T*, T&> iterator;
__RBTreeIterator(const iterator& it)
:_node(it._node)
{}
注意这里iterator直接用T,T*,T&构造的,所以它肯定是一个正常迭代器。
2.修改Map
pair<iterator,bool> insert(const pair<const K, V>& kv)
{
return _t.Insert(kv);
}
直接调用即可
2.[ ]的重载
V& operator[](const K& data )
{
pair<iterator, bool> ret = _t.Insert(make_pair(data,V()));
return ret.first->second;
}
注意map的插入是一个pair类型,所以对于Value我们传了一个默认值进去。
总结
以上就是Map和Set模拟实现中的主要问题所在。
完整版代码存放在Git-ee上:Map,Set模拟实现
本人小白一枚,有问题还望各位大佬指正!!!