之前介绍过标准库中的顺序容器,顺序容器是元素在内存中按照一定顺序进行排列的,都是按线性结构进行排列。除了顺序容器外,c++中还有关联容器。与顺序容器不同的是,关联容器中元素是按照关键字来保存和访问的。与之相对的顺序容器是按它们在容器中的位置来顺序的保存和访问的。
关联容器支持高效的查找和访问。两个主要的关联容器类型是map和set。
标准库提供8种关联容器,这8个容器见的不同体现在3个维度
- 或者是一个set,或者是一个map
- 或者要求不重复的关键字,或者允许重复关键字
- 按顺序保存元素或者无序保存
允许重复关键字的容器都包含单词 multi ,不保持关键字按顺序存储的容器的名字都以单词unordered 开头
这8中容器分别是 map、set、multimap、multiset、unordered_map、unordered_set、unordered_multimap、unordered_multiset
关联容器概述
关联容器不支持顺序容器中的位置相关操作,例如 push_back、push_front。原因是关联容器是按照关键字存储的,这些操作对关联容器没有意义
对于map、multimap、set、multiset 关键字类型必须定义元素的比较方法。默认情况下标准库使用关键字类型的< 运算符来比较两个关键字
在介绍关联容器的操作之前,我们需要了解名为pair的标准库类型。
一个pair保存两个数据成员。类似容器,pair是一个用来生成特定类型的模板。当创建一个pair时,需要提供两个类型名,pair的数据成员将具有对应的类型。两个类型不要求一样
pair<string, string> anon;
pair<string, size_t> word_count;
pair<string, vector<int>> line;
初始化时,会调用类型的默认构造函数进行初始化,也可以为每个成员提供初始化器:
pair<string, string> author{"James", "Joyce"};
pair 的数据成员是public的,两个成员分别命名为first和second。我们用普通的成员访问符号来访问它们。
关联容器的操作
关联容器定义了额外的类型别名
- key_type: 此容器类型的关键字类型
- mapped_type: 每个关键字关联的类型:只适用与map
- value_type: 对于set,与key_value 相同;对于map,为
pair<const key_type, mapped_type>
set<string>::value_type v1; //v1 是一个string
set<string>::key_value v2; //v2 是一个string
map<string, int>::value_type v3; //v3 是一个pair<const string, int>
map<string, int>::key_type v4; //v4 是一个string
map<string, int>::mapped_type v5; //v5 是一个int
我们使用作用域运算符来提取一个类型的成员。
当解引用一个关联容器的迭代器的时候,会得到一个类型为容器的value_type 的值的引用。对map而言,value_type 是一个pair 类型,其first成员保存const的关键字,second成员保存值
auto map_it = word_count.begin();
cout << map_it->first;
cout << " " << map_it->second;
map_it->first = "new key"; //错误 first是const类型
++map_it->second;
set 的迭代器是const的。关联容器中键值是无法通过迭代器进行修改的。只能通过迭代器读,每次修改键值都会导致容器中元素的重新排序。因此不允许通过迭代修改键值
我们通常不能对关联容器使用泛型算法。关键字是const这一特性意味着不能将关联容器传递给修改或者重排容器元素的算法。关联容器可以使用只读取元素的算法。但是很多这类算法都要搜索序列。由于关联容器中的元素不能通过它们的关键字进行快速查找,因此对其使用泛型算法几乎总是一个坏主意
关联容器中有一个find的成员,我们可以使用find算法来根据关键字查找元素。
关联容器的insert成员可以向容器中添加一个元素或者元素范围。因为set和map无法包含关键字重复的元素,因此插入已存在的元素对容器没有任何影响
vector<int> ivec = {2, 4, 6, 8, 2, 4, 6, 8}; //ivec 有8个元素
set<int> set2;
set2.insert(ivec.cbegin(), ivec.cend()); //set2 现在有4个元素
set2.insert({1, 3, 5, 7, 1, 3, 5, 7}); //set2 现在有8个元素
对一个map执行insert操作时,需要记住元素类型是pair。通常在insert的参数列表中创建一个pair
//向word_count 插入word的4种方法
word_count.insert({word, 1});
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t)>(word, 1));
word_count.insert(map<string, size_t>::value_type(word, 1));
insert 的返回值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,告诉我们插入是否成功。pair的first成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值,指出元素是插入成功还是已经存在于容器中。如果关键字已经存在于容器中,则insert什么也不做,且返回值中的bool部分为false。如果关键字不存在,元素被插入容器,且bool值为true
对于允许重复关键字的容器,接受单个元素的insert操作返回一个指向新元素的迭代器。这里无须返回一个bool值,因为insert总是向这类容器中加入一个新元素
关联容器定义了三个版本的erase。可以通过传入一个迭代器或者一个迭代器对来删除一个元素或者一个元素范围。如果指定的元素被删除,函数返回void
关联容器提供一个额外的erase函数,它允许传入一个key_type 参数,删除所有匹配给定关键字的元素,返回实际删除元素的数量。
对于map和unordered_map 容器提供了下标运算,下标中填入key_type的值,得到value_type,如果关键字不在map中,会为它创建一个元素并插入map中。
使用容器的find访问关联容器,传入key_type,如果能找到对应值,返回一个指向对应元素的迭代器,否则返回一个指向容器end()位置的迭代器的
使用容器的count方法,传入key_type,返回容器中相同关键字元素的个数
对于map使用find代替下标操作,使用下标如果未找到对应关键字则会插入一个新的元素,这样会产生未知行为。如果我们只想知道一个给定关键字是否在map中,而不想改变map,这种情况下应该使用find。
对于不允许存储重复关键字的关联容器来说,通过关键字查找元素只会找到一个,而对于允许重复关键字的关联容器来说,会返回第一个元素的迭代器,而相同关键字的元素会相邻存储。在遍历所有相同关键字的元素时,可以首先使用find找到第一个元素的迭代器,然后使用count找到公有多少个元素。最后循环递增迭代器即可访问到所有相同关键字的元素
string search_item("Alain de Botton");
auto enteris = authors.count(search_item);
auto iter = authors.find(search_item);
while(enteris)
{
cout << iter->second << endl;
++iter;
--enteris;
}
除了上述方法,还可以使用 lower_bound和 upper_bound;lower_bound 指向第一个对应的元素,upper_bound 指向匹配的最后一个元素的下一个位置。如果未找到对应元素,则二者指向同一个迭代器,指向一个不影响排序的关键字插入位置
for(auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item); beg != end; ++beg)
{
cout << beg->second << endl;
}
解决此问题的最后一个方法是直接使用容器的equal_range函数。该函数返回一个pair,保存的是两个迭代器。指向的位置与 lower_bound 和 upper_bound 相同
解决此问题的最后一个方法是直接使用容器的equal_range函数。该函数返回一个pair,保存的是两个迭代器。指向的位置与 lower_bound 和 upper_bound 相同
for(auto pos = authors.equal_range(search_item); pos.first != pos.end; ++pos.first)
{
cout << pos.first->second << endl;
}
无序容器
新标准中定义了4种无序关联容器,这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的 == 运算符。在关键字类型的元素没有明显的序关系时,无序容器是非常有用的。
对于自定义类型的关键字,无法直接在无序容器中使用,还需要提供该类型的hash操作。