C++容器篇——set和map容器
1. 关联式容器
像vector/list/dequeu/forward_list
这些容器称为序列式容器,其底层为线性序列的数据结构,存储的数据是元素本身。
关联式容器与其他容器相比同样也是存储数据的,不过是采用<key,value>
结构的键值对,在数据检索时比序列式容器的效率高。
2. 键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key
和value
,key表示键值,value表示与key对应的信息。下面是关于键值对的定义:
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)
{}
};
3. 树形结构的关联式容器
树形结构的关联式容器主要有四种:map、set、multimap、multiset
。这四种容器都是以红黑树作为底层结构,不懂可以参考我写的数据结构之红黑树(C++实现),容器的元素是一个有序的系列。
3.1 set的介绍
- set是按照一定次序存储元素的容器。(有序)
- set中,元素的value就是key,并且每个value必须唯一的。set中的元素不能在容器中修改,只能从容器中插入或者删除它们。(唯一)
- set内部,set中的元素总是按照内部比较对象所指示的特定严格弱排序准则进行排序。(比较)
- set容器通过key访问单个元素比
unordered_set
慢,但它们允许根据数据对子集进行直接迭代。(访问) - set的底层采用红黑树实现。(实现)
注意:
- 与
map/multimap
不同,map/multimap
是存储真正的键值对<key,value>
,set只存放value,但底层实际存放的是<value,value>
结构。 - set插入元素的时候,只需要插入value值即可。
- set中的元素不可以重复。
- 使用set的迭代器遍历set的元素,是可以获得有序序列。
- set中的元素默认是从小到大排序的。
- set查找元素的时间复杂度为
O(log_2(n))
。 - set的元素不允许修改,会导致底层的红黑树结构错误。
- set的底层使用红黑树实现。
3.2 set的使用
set必须包含头文件#include <set>
,并且属于std
命名空间里面。
int main()
{
set<int> s;
s.insert(1);
s.insert(7);
s.insert(3);
s.insert(5);
s.insert(2);
auto it = s.begin();
while(it != s.end())
{
cout << *it << " "; // 1 2 3 5 7
++it;
}
cout << endl;
}
这是一个简单使用set容器的方法,与其他容器几乎一致,并且迭代器只存储了value
值,故可以直接进行*
解引用操作,然后++it
表示移动到下一个元素。最终的结果也是满足set的特性,是有序排序的。
3.2.1 set的定义
set的构造函数:
构造函数 | 接口说明 |
---|---|
set(); | 无参构造 |
set(const set& x); | 拷贝构造 |
set(InputIterator first,InputIterator last); | 迭代器构造 |
- 第一个是无参构造,这个时候容器里面并没有存放任何数据。但是需要通过
<>
来指定容器存放的类型。- 第二个是拷贝构造。
- 第三个是通过迭代器(输入迭代器)来构造并初始化。
int main()
{
set<int> s;
set<int> s1(s);
vector<int> v{1,5,2,4,3,0};
set<int> s2(v.begin(), v.end());
}
3.2.2 set的迭代器
迭代器其实本质上是指针,但是是对指针进行了封装。我们在使用C语言的时候,常常因为指针,甚至多级指针,导致代码异常复杂难看。容器里的迭代器对指针进行改造之后,使用起来更加方便。set迭代器其实调用的是底层红黑树的迭代器。
set获取迭代器:
迭代器的使用 | 使用说明 |
---|---|
begin() | 返回指向开始位置的迭代器 |
end() | 返回指向末尾元素的下一个位置的迭代器 |
cbegin() | 返回指向开始并且为常量的迭代器 |
cend() | 返回指向末尾元素的下一个位置的并且为常量的迭代器 |
rbegin() | 返回逆置迭代器,指向末尾元素下一个位置,操作都是往相反反向 |
rend() | 返回逆置迭代器,指向开头元素的位置,操作都是往相反反向 |
crbegin() | 返回逆置迭代器,指向末尾元素下一个位置,操作都是往相反反向,并且为常量属性 |
crend() | 返回逆置迭代器,指向开头元素的位置,操作都是往相反反向,并且为常量属性 |
3.2.3 set的容量
容量说明 | 接口说明 |
---|---|
size | 获取容器中实际的个数 |
empty | 判断是否为空 |
3.2.4 set增删查
增删改查 | 接口说明 |
---|---|
insert(const value_type& x) | 在set中插入元素x,实际上插入的是<x,x>构成的键值对,如果成功,返回<该元素在set中的,true> ,否则,返回<x在set的位置,false> 。 |
erase(iterator pos) | 删除set中pos位置的元素。 |
erase(const value_type& x) | 删除set中值为x的元素,返回删除元素的个数。 |
swap(set& st) | 交换set中的元素 |
find(const value_type& x) | 返回set中值为x的元素的位置 |
count(const value_type& x) | 返回set中值为x的元素的个数 |
clear | 清除set的元素 |
下面是相关代码演示:
#include <set>
void TestSet()
{
// 用数组array中的元素构造set
int array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
set<int> s(array, array+sizeof(array)/sizeof(array));
cout << s.size() << endl;
// 正向打印set中的元素,从打印结果中可以看出:set可去重
for (auto& e : s)
cout << e << " ";
cout << endl;
// 使用迭代器逆向打印set中的元素
for (auto it = s.rbegin(); it != s.rend(); ++it)
cout << *it << " ";
cout << endl;
// set中值为3的元素出现了几次
cout << s.count(3) << endl;
}
3.3 map的介绍
- map是关联容器,他是按照特定的次序(根据key比较)存储由
<key,value>
键值对组合而成的元素。 - 在map中,键值key是用于排序且唯一表示元素,而值value中存储与此键值key关联的内容。键值key和value的类型可以不同,并且在map内部,通常会将key和value用pair进行绑定。
- map中键值访问单个元素的速度要比
unordered_map
容器慢,但map允许根据顺序对元素进行直接迭代,获得一个有序的序列。 - map支持下标访问符访问key对应的value,
map[key]=value
。 - map的底层同样也是红黑树。
3.4 map的使用
map必须包含头文件#include <map>
,并且属于std
命名空间里面。
int main()
{
map<int, string> m;
m[0] = "hello";
m[1] = "world";
m[2] = ".";
auto it = m.begin();
while (it != m.end())
{
cout << it->first << " : " << it->second << " "; // 0 : hello 1 : world 2 : .
++it;
}
cout << endl;
}
这是一个简单使用map容器的方法,与set容器不同的地方在于它存的是一个<key,value>
型,所以构造函数的模板参数需要分别传入key和value的类型,并且可以采用下表进行访问和插入元素,迭代器本质上是一个pair<key,value>
类型,所以访问的方式与set也有所不同,first
是key值,second
是value值,最后,迭代器进行访问元素的时候会按照key排序之后的顺序进行打印。
3.4.1 map的定义
map的构造函数:
构造函数 | 接口说明 |
---|---|
map(); | 无参构造 |
- 第一个是无参构造,构造一个空的map,传入的模板参数有key,value的类型,比较器的类型(默认是less)。
int main()
{
map<string,int> m;
map<string,int,greater<string>> m2;
}
3.4.2 map的迭代器
map迭代器与set的迭代器相似,只不过迭代器获取的是一个pair<key,value>
的类型,访问元素不可以直接通过解引用进行访问,需要按照pair
的方式。
map获取迭代器:
迭代器的使用 | 使用说明 |
---|---|
begin() | 返回指向开始位置的迭代器 |
end() | 返回指向末尾元素的下一个位置的迭代器 |
cbegin() | 返回指向开始并且为常量的迭代器 |
cend() | 返回指向末尾元素的下一个位置的并且为常量的迭代器 |
rbegin() | 返回逆置迭代器,指向末尾元素下一个位置,操作都是往相反反向 |
rend() | 返回逆置迭代器,指向开头元素的位置,操作都是往相反反向 |
crbegin() | 返回逆置迭代器,指向末尾元素下一个位置,操作都是往相反反向,并且为常量属性 |
crend() | 返回逆置迭代器,指向开头元素的位置,操作都是往相反反向,并且为常量属性 |
3.4.3 map的容量
容量说明 | 接口说明 |
---|---|
size | 获取容器中实际的个数 |
empty | 判断是否为空 |
3.4.4 map的元素访问
函数 | 接口说明 |
---|---|
mapped_type& operator[](const key_type& k) | 获取key对应的value值,如果key存在,则无需插入到map中,直接返回key对应的value值,如果key不存在,会将对应的key和value值插入到map中。 |
上面这个接口等价于
(*((this->insert(make_pair(k,mapped_type()))).first)).second;
首先,对key和value值构建键值对pair
,然后调用map中的insert
接口,返回一个pair<iterator,bool>
,假如key值不存在,那么iterator
就是插入之后的迭代器的位置,bool
是表示插入成功,然后迭代器本质上存的就是pair<key,value>
,所以最后对返回的迭代器拿去second
就是获取value值。假如key值已经存在,iterator
也会返回key值对应迭代器的位置,然后访问迭代器的second
也同样是value值。
3.4.5 map的修改
增删改查 | 接口说明 |
---|---|
pair<iterator,bool> insert(const value_type& x) | 在map中插入键值对<key,value>,如果成功,返回新插入位置的迭代器,true ,否则返回,key对应位置的迭代器,false 。 |
erase(iterator pos) | 删除map中pos位置的元素。 |
erase(const value_type& x) | 删除map中键值对为x的元素 |
swap(map& m) | 交换map中的元素 |
find(const key_type& x) | 在map中查找key为x的元素,找到返回该元素位置的迭代器,找不到返回end() |
count(const key_type& x) | 返回map中key值为x的元素的个数 |
clear | 清除map的元素 |
#include <iostream>
#include <string>
#include <map>
using namespace std;
void TestMap()
{
map<string, string> m;
// 向map中插入元素的方式:
// 将键值对<"peach","桃子">插入map中,用pair直接来构造键值对
m.insert(pair<string, string>("peach", "桃子"));
// 将键值对<"peach","桃子">插入map中,用make_pair函数来构造键值对
m.insert(make_pair("banan", "香蕉"));
// 借用operator[]向map中插入元素
/*
operator[]的原理是:
用<key, T()>构造一个键值对,然后调用insert()函数将该键值对插入到map中
如果key已经存在,插入失败,insert函数返回该key所在位置的迭代器
如果key不存在,插入成功,insert函数返回新插入元素所在位置的迭代器
operator[]函数最后将insert返回值键值对中的value返回
*/
// 将<"apple", "">插入map中,插入成功,返回value的引用,将“苹果”赋值给该引用结果
m["apple"] = "苹果";
// key不存在时抛异常
//m.at("waterme") = "水蜜桃";
cout << m.size() << endl;
// 用迭代器去遍历map中的元素,可以得到一个按照key排序的序列
for (auto& e : m)
cout << e.first << "--->" << e.second << endl;
cout << endl;
// map中的键值对key一定是唯一的,如果key存在将插入失败
auto ret = m.insert(make_pair("peach", "桃色"));
if (ret.second)
cout << "<peach, 桃色>不在map中, 已经插入" << endl;
else
cout << "键值为peach的元素已经存在:" << ret.first->first << "--->"
<< ret.first->second << " 插入失败" << endl;
// 删除key为"apple"的元素
m.erase("apple");
if (1 == m.count("apple"))
cout << "apple还在" << endl;
else
cout << "apple被吃了" << endl;
}
[总结]:
- map中的元素是键值对
- map中的key是唯一,并且不能修改
- map默认是按照从小到大的顺序对key进行排序。
- map如果用迭代器遍历元素,可以获得一个key有序的序列。
- map的底层为红黑树,查找效率为
O(log_2(n))
。 - map支持
[]操作符
,可以通过下标进行插入和查找。
3.5 multiset和multimap
这两个容器的与set
和map
区别在于可以存储多个key值,重点在于count
接口,如果是set
和map
,只会返回1和0,而multiset
和multimap
可以返回具体的数量。
4. 红黑树模拟实现STL的map与set
在看这之前,最好对红黑树有些许了解,可以看我的博客数据结构之红黑树(C++实现)。
4.1 红黑树的迭代器
迭代器的好处是可以方便遍历,并且数据结构的底层实现与用户透明。如果想要给红黑树添加迭代器,需要考虑以下问题:
-
begin()与end()
我们知道begin()和end()是一段前闭右开区间,红黑树的最小结点在最左侧结点,故对红黑树一直往左子树遍历,直到遇到
cur->left == nullptr
时。end()为最大结点的下一个位置,我们知道end()结点的下一个是为空,故返回nullptr
即可。 -
operator++()和operator–()
- 当前进行++操作,如果当前结点的右子树存在,那么就找到右孩子的最左侧结点,故为当前结点的下一个结点。如果当前结点的右子树不存在,那么就从祖先结点中去寻找,找到当前结点不等于父节点的右孩子的那个结点
cur != parent->right
,故当前结点为下一个结点。 - 当前进行–操作,如果当前结点的左子树存在,那么就找到左子树的最右侧的结点,故为当前结点的前一个结点。如果当前结点的左子树不存在,那么就从祖先结点中去寻找,找到当前结点不等于父节点的左孩子的那个结点
cur != parent->left
,故当前结点为上一个结点。
- 当前进行++操作,如果当前结点的右子树存在,那么就找到右孩子的最左侧结点,故为当前结点的下一个结点。如果当前结点的右子树不存在,那么就从祖先结点中去寻找,找到当前结点不等于父节点的右孩子的那个结点
-
迭代器实现如下:
template<class T, class Ref, class Ptr> struct __RBTreeIterator { typedef RBTreeNode<T> Node; typedef __RBTreeIterator<T,Ref,Ptr> Self; __RBTreeIterator(Node *node) : _node(node) {} Ref operator*() { return _node->_data; } Ptr operator->() { return &_node->_data; } bool operator!=(const Self& s) const { return _node != s._node; } bool operator==(const Self& s) const { return _node == s._node; } Self& operator++() { if(_node->_right) { // 下一个是右子树的最左节点 Node *left = _node->_right; while(left->_left) left = left->_left; _node = left; } else { // 找祖先里面孩子不是祖先最右的那个 Node *parent = _node->_parent; Node *cur = _node; while(parent && cur == parent->_right) { cur = cur->_parent; parent = parent->_parent; } _node = parent; } return *this; } Self& operator--() { if(_node->_left) { // 下一个是右子树的最左节点 Node *right = _node->_left; while(right->_right) right = right->_right; _node = right; } else { // 找祖先里面孩子不是祖先最左的那个 Node *parent = _node->_parent; Node *cur = _node; while(parent && cur == parent->_left) { cur = cur->_parent; parent = parent->_parent; } _node = parent; } return *this; } Node *_node; }; typedef RBTreeNode<T> Node; typedef __RBTreeIterator<T,T&,T*> iterator; iterator begin() { Node* left = _root; while(left && left->_left) left = left->_left; return iterator(left); } iterator end() { return iterator(nullptr); }
4.2 改造红黑树
/*
因为关联式容器存储的是<key,value>的键值对,而set和map的底层都是红黑树。
那么我们知道set其实存的是<value,value>,而map才是存储<key,value>。
那么,这样会导致我们如果按照原先的红黑树写法,就必须设计两个红黑树。
为了方便,我们设计一个仿函数类,这个类是通过value来获取key的一个仿函数。
*/
// 红黑树的结点只存储value值
template<class T>
struct RBTreeNode {
RBTreeNode<T> *_left;
RBTreeNode<T> *_right;
RBTreeNode<T> *_parent;
T _data;
Colour _col;
RBTreeNode(const T &data)
: _left(nullptr), _right(nullptr), _parent(nullptr), _data(data) {}
};
template<class K, class T, class KeyOfT>
struct RBTree {
typedef RBTreeNode<T> Node;
typedef __RBTreeIterator<T,T&,T*> iterator;
private:
Node *_root = nullptr;
};
4.3 map的模拟实现
namespace Ming
{
template<class K,class V>
class map
{
// 设计一个map的仿函数类
struct MapKeyOfT
{
const K& operator()(const pair<K,V>& kv)
{
return kv.first;
}
};
public:
// map的value值其实是对key和value进行包装的一个键值对
typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::iterator iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
pair<iterator,bool> insert(const pair<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<K,V>,MapKeyOfT> _t;
};
}
4.4 set的模拟实现
namespace Ming {
template<class K>
class set {
struct SetKeyOfT {
const K &operator()(const K &key) {
return key;
}
};
public:
// set的key和value都是同一个值
typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;
iterator begin() {
return _t.begin();
}
iterator end() {
return _t.end();
}
pair<iterator,bool> insert(const K &key) {
return _t.Insert(key);
}
private:
RBTree<K, K, SetKeyOfT> _t;
};
}