前言
在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。
那么我们今天继续探索STL中的另一类容器——关联式容器:map和set。
关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。
一:map
键值对其实就是用来表示映射关系的一种结构,结构中包含一个Key和一个Value,Key代表着索引的键值,而Value代表着索引映射的数据,通过一个Key来映射一个Value。
代码实现:
template <class T1, class T2>
struct pair
{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair(): first(T1()), second(T2())
{}
pair(const T1& a, const T2& b): first(a), second(b)
{}
};
1.1 map的基本操作
代码演示:
- map的插入操作需要插入pair结构
- map的迭代器输出需要输出first和second
- map被用来统计次数
#include<iostream>
#include<map>
#include<string>
using namespace std;
void TestMap(){
map<int,int> m;
m.insert(pair<int, int>(1, 1));
m.insert(pair<int, int>(3, 3));
m.insert(pair<int, int>(2, 2));
m.insert(make_pair(4, 4));
map<int, int>::iterator it = m.begin();
while (it != m.end()){
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
for (auto& e : m){
cout << e.first << ":" << e.second << endl;
}
}
void Count(){
string strArray[] = { "西瓜", "苹果", "西瓜", "樱桃", "西瓜", "苹果" };
map<string, int> countMap;
for (auto& str : strArray){
pair<map<string, int>::iterator,bool> ret = countMap.insert(make_pair(str, 1));
if (ret.second == false){
// 插入失败
// 返回结点所在迭代器
ret.first->second++;
}
}
for (auto& e : countMap){
cout << e.first << ":" << e.second << endl;
}
}
void TestCount(){
string strArray[] = { "西瓜", "苹果", "西瓜", "樱桃", "西瓜", "苹果" };
map<string, int> countMap;
for (auto& str : strArray){
countMap[str]++;
}
for (auto& e : countMap){
cout << e.first << ":" << e.second << endl;
}
}
int main(){
//TestMap();
//TestCount();
Count();
return 0;
}
1.2 multimap
map中的元素key值不允许相同,由此便延伸出了multimap,multimap中允许元素key值相同,要注意的是insert相关的接口永远都会插入新的元素,并不会存在因为key值存在而插入失败的情况。
multimap中没有operator[ ],当有多个相同的key_type时,会产生数据的二义性,不知道返回哪个key_type对应的mapped_type。
1.3 map中operator[ ]的原理
代码演示:
mapped_type& operator[](const key_type& k){
return (*((this->insert(make_pair(k, mappe_type()))).first)).second;
}
原理分析:
若插入成功(map中无插入结点),insert返回新插入结点的迭代器
若插入失败(map中有插入结点),insert返回原有结点的迭代器
将返回的迭代器解引用得到pair<key_type,mapped_type>结构,pair的second就是value
问题:map的operator[ ]为什么不用find实现呢?
使用insert实现,若operator[ ]中的key_type不存在,可以将key_type直接插入。
小结:map中operator[ ]有哪些作用?
1.插入
2.查找key_type对应的映射对象
3.修改key_type对应的映射对象
二:set
set是一种树形结构的关联式容器,按照一定的次序存储数据,底层是用二叉搜索树(红黑树) 实现的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
2.1 set的基本操作
代码演示:
- 迭代器遍历:排序+去重
- 拷贝:深拷贝树形结构
- find:查找效率为O(logN)高于算法find
#include<iostream>
#include<set>
using namespace std;
void TestSet(){
set<int> s;
s.insert(3);
s.insert(1);
s.insert(4);
s.insert(3);
s.insert(7);
// 排序+去重
set<int>::iterator it = s.begin();
while (it != s.end()){
cout << *it << " ";
++it;
}
cout << endl;
// 拷贝(深拷贝树形结构)
set<int> copy(s);
for (const auto& e : s){
cout << e << " ";
}
cout << endl;
// 查找删除迭代器
// O(logN)
set<int>::iterator pos = s.find(3);
// 若找到则删除
// 若没找到返回end位置进行删除非法所以必须加以判断
if (pos != s.end()){
s.erase(pos);
}
for (const auto& e : s){
cout << e << " ";
}
cout << endl;
// 查找删除值
s.erase(4);
for (const auto& e : s){
cout << e << " ";
}
cout << endl;
}
int main(){
TestSet();
return 0;
}
运行结果:
1 3 4 7
1 3 4 7
1 4 7
1 7
2.2 multiset
set中的元素key值不允许相同,由此便延伸出了multiset,multiset中允许元素key值相同,要注意的是insert相关的接口永远都会插入新的元素,并不会存在因为key值存在而插入失败的情况。
三:map和set的模拟实现
3.1 仿函数
map和set的底层是一颗红黑树,我们需要使用一棵红黑树来实现KV模型的map和K模型的set,为了避免冗余,我们需要代码复用,此时就要借助仿函数来解决这个问题,我们需要三个模板参数。
template<class K, class T, class KOfV>
对于KV模型的map来说,它的K即是key的类型,而T则是键值对pair<K, V>。
对于K模型的set来说,它的K即是key的类型,而T也是key的类型。
我们还需要考虑如何从参数中获取key?
K模型的set还好说,因为它的参数T本身就是key。
KV模型的map则存在了问题,因为它参数T是一个pair<K,V>的键值对,他的key是参数的first,此时两边就会存在差异。
针对上述问题我们会增加一个仿函数作为模板参数,让使用者自己提供从参数T中获取key的方法。
map:
struct MapKeyOfValue{
const K& operator()(const pair<K, V>& kv){
return kv.first;
}
};
set:
struct SetKeyOfValue{
const K & operator()(const K & key){
return key;
}
};
3.2 迭代器
迭代器的遍历基于红黑树的中序遍历,中序的起点和终点分别是红黑树左子树的最左结点和红黑树右子树的最右结点的下一个位置。
那么右子树的最右结点的下一个位置该如何处理?
迭代器的最后一个位置一般都是不存储任何数据的多余的空间,并且那块空间是可以进行操作的,因为我们可能需要通过对end来进行倒序遍历,所以直接给nullptr也是行不通的。
针对上述问题我们可以设计一个这样的结构,通过添加一个head结点来解决这个问题。
头节点的左子树指向最左结点,头结点的右子树指向最右结点。同时,头节点与根节点互相为对方的父结点。并且将头结点设置为红色,和根结点区分开来。
此时,begin为最左结点,end即为head结点。所以迭代器的遍历就从最左结点出发,按照中序遍历走完走到最右结点,然后当走到尽头时,下一步就是走到end也就是head结点。而如果是从end倒序遍历,就直接让head访问到他的右子树即可。
template<class T, class Ptr, class Ref>
class __RBTreeIterator
{
public:
typedef RBTreeNode<T> Node;
typedef __RBTreeIterator<T, Ptr, Ref> Self;
__RBTreeIterator(Node* node) : _node(node)
{}
Ref operator *()
{
return _node->_data;
}
//调用时其实调用的是->->,这里返回的是数据对象的指针,然后再从指针中取数据。编译器优化所以只看到一个
Ptr operator ->()
{
return &_node->_data;
}
//红黑树迭代器的++ --其实就是其在中序遍历中的次序变化
Self& operator++()
{
/*
中序遍历:先访问所有节点的左子树,接着访问这些节点的右子树。
而++,其实就是其在中序遍历的下一个位置
1.如果当前节点的右子树不为空,则说明当前节点的右子树还没遍历完,下一个位置则是右子树的最左节点。
2.如果此时右子树为空,则说明当前节点的所有子树访问完,但是当前节点也可能为其他树的右子树,所以一直往上,找到孩子为父亲的左子树的结点,那个父亲的位置则是下一个位置
*/
if (_node->_right)
{
Node* cur = _node->_right;
//找到右子树的最左节点
while (cur->_left)
{
cur = cur->_left;
}
_node = cur;
}
else
{
Node* parent = _node->_parent;
Node* cur = _node;
//往上找,直到父节点为空或者孩子节点为父节点的左子树
while (parent->_right == cur)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
Self operator++(int)
{
Self temp(*this);
++(*this);
return temp;
}
/*
而--,其实就是其在中序遍历的上一个位置,思路和++差不多
1.如果当前节点的左子树不为空,则访问左子树的最右节点
2.如果此时左子树为空,则说明子树的最左节点已经访问,接着就往上查找,找到孩子为父亲的右节点的位置,那个父亲就是下一个位置
*/
Self& operator--()
{
if (_node->_parent->_parent == _node && _node->_color == RED)
{
//如果此节点为头结点,则说明--应该回到最右子树的位置,也就是头结点的右子树,而因为头结点和根节点形成闭环,所以当
//_node->_parent->_parent == _node,并且color为红时,说明为_head。
_node = _node->_right;
}
else if (_node->_left)
{
Node* cur = _node->_left;
//找到左子树的最右节点
while (cur && cur->_right)
{
cur = cur->_right;
}
_node = cur;
}
else
{
Node* parent = _node->_parent;
Node* cur = _node;
//往上找,直到父节点为空或者孩子节点为父节点的右子树
while (parent->_right != cur)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
Self operator--(int)
{
Self temp(*this);
--(*this);
return temp;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
private:
Node* _node;
};