关联容器的基本介绍
关联容器支持高效的关键字查找和访问。map和set是最主要关联容器。关联容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。C++标准库中提供了8个关联容器:
关联容器的类型 | |
按关键字有序保存元素 | |
map | 关联数组;保存键值对 |
set | 关键字即值,即只保存关键字的容器 |
multimap | 关键字可重复出现的map |
multiset | 关键字可重复出现的set |
无序集合 | |
unordered_map | 用哈希函数组织的map |
unordered_set | 用哈希函数组织的set |
unordered_multimap | 哈希组织的map;关键字可重复出现 |
unordered_multiset | 哈希组织的set;关键字可重复出现 |
其中,类型map和multimap定义在头文件map中;set和multiset定义在头文件set中;无序容器则定义在头文件unordered_map和unordered_set中。本文重点讨论map与set
键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key相对应的信息。比如 可以将一个人的名字作为关键字key,将其电话号码作为值value,这样我们就可以通过人名来找到对应的电话号码。
pair类型
pair是一个标准库类型,它定义在头文件utility中。pair文档
一个pair保存两个数据成员。类似容器,pair是一个用来生成特定类型的模板,下面是pair的原型
// SGI-STL中关于键值对的定义
template <class T1, class T2>
struct pair
{
typedef T1 first_type;
typedef T2 second_type;
pair() // 构造
: first(T1())
, second(T2())
{}
pair(const T1& a, const T2& b) // 拷贝构造
: first(a)
, second(b)
{}
T1 first; // 成员变量
T2 second;
};
当创建一个pair时,我们必须提供两个类型名,pair的数据成员将具有对应的类型
// 代码示例
pair<string, string> a; // 保存两个string
pair<string, int> b; // 保存一个string和一个int
pair<string, vector<int>> c; // 保存一个string和vector<int>
pair的默认构造函数会对数据成员进行值初始化。因此a是一个包含两个空string的pair;b中的int成员值初始化为0,而string成员初始化为空;c保存了一个空string和一个空的vector。
我们也可以为每个成员提供初始化器:
// 创建一个名为object的pair,两个成员分别被初始化为“xxxx”和2
pair<string, int> object {"xxxx", 2};
pair的常见操作 | |
pair<T1, T2> p; | p是一个pair,两类型分别为T1和T2的成员进行了 值初始化 |
pair<T1, T2> p (v1, v2); | p是一个成员类型为T1和T2的pair;first和second成员分别用v1,v2初始化 |
pair<T1, T2> p = {v1, v2}; | 等价于 p (v1, v2) |
make_pair(v1, v2); | 返回一个用v1和v2初始化的pair。pair的类型从v1和v2的类型推断出来 |
set
set的基本介绍
- set是按照一定次序存储元素的容器
- 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
- 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格的排序准则(默认是小于)进行排序。
- set在底层是用二叉搜索树(红黑树)实现的。
set的基本使用
set的构造
函数声明 | 功能介绍 |
set (const Compare& comp = Compare(), const Allocator& = Allocator() ); | 构造空的set |
set (InputIterator first, InputIterator last, const Compare& comp = Compare(), const Allocator& = Allocator() ); | 用[first, last)区间中的元素构造set |
set ( const set<Key,Compare,Allocator>& x); | set的拷贝构造 |
// set构造
std::set<int> first; // empty set of ints
int myints[]= {10,20,30,40,50};
std::set<int> second (myints,myints+5); // range
std::set<int> third (second); // a copy of second
std::set<int> fourth (second.begin(), second.end()); // iterator ctor.
set的迭代器
虽然set同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素,因为set中的关键字是const的。可以用一个set迭代器来读取元素的值,但不能修改。
set<int> s = {0,1,2,3,4,5,6,7,8,9};
set<int>::iterator it = s.begin();
if(it != s.end())
{
//*it = 20; // 错误,set中的关键字是只读的
cout << *it << endl; // 正确,可以读关键字
}
set的修改操作
函数声明 | 功能介绍 |
pair<iterator,bool> insert (const value_type& k ) | 在set中插入元素k,实际插入的是<k, k>构成的 键值对,如果插入成功,返回<该元素在set中的 位置,true>,如果插入失败,说明k在set中已经 存在,返回<k在set中的位置,false> |
void erase ( iterator position ) | 删除set中position位置上的元素 |
void erase ( iterator first, iterator last ) | 删除set中[first, last)区间中的元素 |
size_type erase ( const key_type& k ) | 删除set中值为k的元素,返回删除的元素的个数 |
插入操作
insert操作向容器中添加一个元素或一个元素范围。
vector<int> v = {2,4,6,8,2,4,6,8};
set<int> s;
s.insert(v.begin(), v.end()); // 迭代器
s.insert({1,3,5,7,1,3,5,7}); // 初始化器列表
insert有两个版本,分别接受一对迭代器,或是一个初始化器列表,这两个版本的行为类似对应的构造函数。
删除操作
set定义了三个版本的erase,如上表所示。与vector(序列式容器)一样,我们可以通过传递给erase一个迭代器 或者 一个元素范围。这两个版本的erase与对应的序列式容器的操作非常相似:指定元素被删除,函数返回void。
set提供了一个额外的erase操作,此版本删除所有匹配给定关键字的元素(如果存在的话),返回实际删除的元素的数量。就set来说此版本的erase的返回值总是0或者1。若返回0则说明该元素不存在,返回1则说明存在;但是如果在multiset上面,返回值可能会大于1。
set的访问操作
函数说明 | 功能简介 |
iterator find ( const key_type& k ) const | 返回一个迭代器,指向第一个关键字为 k 的元素, 若k不在容器中,则返回尾后迭代器end() |
size_type count ( const key_type& k ) const | 返回关键字等于 k 的元素的数量。 |
set<int> s = {0,1,2,3,4,5,6,7,8,9};
s.find(1); // 返回一个迭代器,指向key == 1的元素
s.find(20); // 返回一个迭代器,其值等于s.end()
s.count(1); // 返回1
s.count(20); // 返回0
map
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支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
- map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。
map的基本使用
map的构造
函数声明 | 功能介绍 |
map() | 构造一个空的map |
map (const map& x); | 拷贝构造 |
map的迭代器
注意:map的value_type是一个pair类型,其first成员保存const的关键字,second成员保存值。我们可以改变pair的值,但是不能改变关键字成员的值!!!
map<string, string> m;
// 使用insert插入元素到m
m.insert(pair<string, string>("peach", "桃子"));
m.insert(make_pair("banan", "香蕉"));
map<string, string>::iterator it = m.begin();
// 遍历map
while(it != m.end())
{
cout << it->first << " " << it->second << endl;// 打印此元素的关键字和值
//it->first = "apple" // 错误,关键字是const
it->second++; // 正确,可以通过迭代器改变值
}
map的修改操作
函数声明 | 功能简介 |
pair<iterator,bool> insert (const value_type& k ) | 在map中插入键值对k,注意 k 是一个键值 对,返回值也是键值对:iterator代表新插入 元素的位置,bool代表释放插入成功 |
void erase ( iterator position ) | 删除map中position位置上的元素 |
void erase ( iterator first, iterator last ) | 删除map中[first, last)区间中的元素 |
size_type erase ( const key_type& k ) | 删除map中值为k的元素,返回删除的元素的个数 |
插入操作
对map进行insert操作时,必须要记住元素类型是一个pair。通常,对于想要插入的数据,并没有一个现成的pair对象。但是可以在insert的参数列表中创建一个pair。
// pair的构造方式
map<string, string> dict;
// 方式一:有名对象
pair<string, string> kv("left", "左边");
dict.insert(kv);
// 方式二:匿名对象
dict.insert(pair < string, string>("right", "右边"));
// 方式三:make_pair函数模板(make_pair帮你构造)
dict.insert(make_pair("up", "上边"));
// 方式四:多参数构造函数-隐式类型转换--中间生成临时对象
pair<string, string> kv1 = { "down", "下边" };
dict.insert({ "down", "下边" });
// 方式五:initializer_list构造
map<string, string> dict = { {"left", "左边"}, {"right", "右边"}, {"up", "上边"} ,{"down", "下边"} };
删除操作
参考set的删除操作
map的访问操作
函数声明 | 功能简介 |
iterator find ( const key_type& k ) | 返回一个迭代器,指向第一个关键字为 k 的元素, 若k不在容器中,则返回尾后迭代器end() |
size_type count ( const key_type& k ) const | 返回key为k的键值在map中的个数,注意map中key 是唯一的,所以该函数的返回值要么为0,要么为1, 因此也可以用该函数来检测一个key是否在map中 |
mapped_type& operator[] (const key_type& k) | 返回key对应的value |
ps. 其中find函数与count函数请参见set
再谈insert
前面我们已经了解过insert函数作用和使用场景,下面我们再来谈谈insert的返回值。
仔细观察insert的函数声明不难发现,其返回值是一个pair。其中pair的first成员是一个迭代器,指向指定关键字的元素;second成员是一个bool值,指出元素是插入成功还是已经存在容器中。如果关键字已在容器中,则insert什么也不做,且其中的bool值为false;如果关键字不存在,元素则被插入到容器中,且bool值为true。
// 单词计数程序
map<string, int> m;
string word;
while(cin >> word)
{
// 插入一个元素,关键字等于word, 值为1
// 若word已经在m中,则insert什么也不做
auto ret = m.insert({word, 1});
if(!ret.second) // word已经在m中
{
++((ret.first)->second); // 递增计数器
}
}
上面代码中的if语句检查返回值的bool部分,若为false,则说明插入操作并未发生。
明确了insert函数,下面我们再来谈谈map的下标操作operator[]。
operator[]
map和unordered_map容器提供了下标运算符,简单说,operator[]就是给我一个key,我返回key对应value的引用。operator[]文档
上面两张图非常详细的介绍了operator[]的基本原理,希望大家能够理解
// 代码演示
map<string, int> m;
m["kevin"] = 1;
// 将会进行下面的操作
// 在m中搜索关键字为kevin的元素,未找到。
// 将一个新的键值对插入到m中,关键字是一个const string,保存kevin。值进行初始化为0
// 提取出新插入的元素,并赋值为1
对于map的下标操作,其行为与数组或vector上的下标操作很不相同:使用一个不在容器中的关键字作为下标,会添加一个具有此关键字的元素到map中。
map的下标操作与其他下标运算符的另一个不同之处就是其返回类型。通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的。但map的下标操作会返回一个key对应value的引用;当解引用map的迭代器时,会得到一个pair对象。
与其他下标运算符相同的是,map的下标运算符返回一个左值。由于返回的是一个左值,所以我们可以读写元素。
cout << m["kevin"]; // 用Kevin作为下标提取元素,会打印出1
++m["kevin"]; // 提取元素,将其增加1
cout << m["kevin"]; // 提取元素并打印,会打印出2