C++ Primer 4 第十章 关联容器

第十章 关联容器

关联容器和顺序容器的本质差别在于:关联容器通过键(key)存储和读取元素,而顺序容器则通过元素在容器中的位置顺序存储和访问元素。

1. 引言:pair 类型

         pair 包含两个数据值。与容器一样,pair 也是一种模板类型。但又与之前介绍的容器不同,在创建 pair 对象时,必须提供两个类型名:pair 对象所包含的两个数据成员各自对应的类型名字

         与其他标准库类型不同,对于 pair 类,可以直接访问其数据成员:其成员都是仅有的,分别命名为 first 和 second。只需使用普通的点操作符——成员访问标志即可访问其成员:

     string firstBook;

     if (author.first == "James" && author.second == "Joyce")

         firstBook = "Stephen Hero";

pairs 类型提供的操作:

         pair<T1, T2> p1; 创建一个空的 pair 对象,它的两个元素分别是 T1 和 T2 类型,采用值初始化

        pair<T1, T2> p1(v1, v2); 创建一个 pair 对象,它的两个元素分别是 T1 和 T2 ,其中 first 成员初始化为 v1,而 second 成员初始化为 v2

         make_pair(v1, v2) 以 v1 和 v2 值创建一个新 pair 对象,其元素类型分别是 v1 和 v2 的类型

        p1 < p2 两个 pair 对象之间的小于运算,其定义遵循字典次序:如果 p1.first < p2.first 或者 !(p2.first < p1.first) && p1.second < p2.second,则返回 true

        p1 == p2如果两个 pair 对象的 first 和 second 成员依次相等,则这两个对象相等。该运算使用其元素的 == 操作符

         p.first 返回 p 中名为 first 的(公有)数据成员

        p.second 返回 p 的名为 second 的(公有)数据成员

2. 关联容器

关联容器共享大部分——但并非全部——的顺序容器操作。关联容器不提供 front、 push_front、 pop_front、back、push_back 以及 pop_back 操作。“容器元素根据键的次序排列”这一事实就是一个重要的结论:在迭代遍历关联容器时,我们可确保按键的顺序的访问元素,而与元素在容器中的存放位置完全无关。

3. map 类型

map 是键-值对的集合。map 类型通常可理解为关联数组(associative array):可使用键作为下标来获取一个值,正如内置数组类型一样。而关联的本质在于元素的值与某个特定的键相关联,而并非通过元素在数组中的位置来获取。

map 对象的定义: 要使用 map 对象,则必须包含 map 头文件。在定义 map 对象时,必须分别指明键和值的类型(value type):

         map<k, v> m; 创建一个名为 m 的空 map 对象,其键和值的类型分别为 k 和 v

        map<k, v> m(m2);创建 m2 的副本 m,m 与 m2 必须有相同的键类型和值类型

        map<k, v> m(b, e);创建 map 类型的对象 m,存储迭代器 b 和 e 标记的范围内所有元素的副本。元素的类型必须能转换为 pair<const k, v>

键类型的约束: 在使用关联容器时,它的键不但有一个类型,而且还有一个相关的比较函数。默认情况下,标准库使用键类型定义的 < 操作符来实现键(key type)的比较。

所用的比较函数必须在键类型上定义严格弱排序(strict weak ordering)。所谓的严格弱排序可理解为键类型数据上的“小于”关系,虽然实际上可以选择将比较函数设计得更复杂。但无论这样的比较函数如何定义,当用于一个键与自身的比较时,肯定会导致 false 结果。此外,在比较两个键时,不能出现相互“小于”的情况,而且,如果 k1“小于”k2,k2“小于”k3,则 k1 必然“小于”k3。对于两个键,如果它们相互之间都不存在“小于”关系,则容器将之视为相同的键。用做 map 对象的键时,可使用任意一个键值来访问相应的元素。

对于键类型,唯一的约束就是必须支持 < 操作符,至于是否支持其他的关系或相等运算,则不作要求。

map 定义的类型: map 对象的元素是键-值对,也即每个元素包含两个部分:键以及由键关联的值。map 的 value_type 就反映了这个事实。value_type 是存储元素的键以及值的 pair 类型,而且键为 const。例如,word_count 数组的 value_type 为 pair<const string, int> 类型。

         map<K, V>::key_type 在 map 容器中,用做索引的键的类型

        map<K, V>::mapped_type 在 map 容器中,键所关联的值的类型

        map<K, V>::value_type 一个 pair 类型,它的 first 元素具有 const map<K, V>::key_type 类型,而 second 元素则为 map<K, V>::mapped_type 类型

map 迭代器进行解引用将产生 pair 类型的对象:

         map<string, int>::iterator map_it = word_count.begin();

     cout << map_it->first;                  // prints the key for this element

     cout << " " << map_it->second;          // prints the value of the element

     map_it->first = "new key";              // error: key is const

     ++map_it->second;     // ok: we can change value through an iterator

map 类额外定义了两种类型:key_type 和 mapped_type,以获得键或值的类型。如同顺序容器一样,可使用作用域操作符(scope operator)来获取类型成员,如 map<string, int>::key_type。

使用下标访问 map 对象:

如同其他下标操作符一样,map 的下标也使用索引(其实就是键)来获取该键所关联的值。如果该键已在容器中,则 map 的下标运算与 vector 的下标运算行为相同:返回该键所关联的值。只有在所查找的键不存在时,map 容器才为该键创建一个新的元素,并将它插入到此 map 对象中。此时,所关联的值采用值初始化:类类型的元素用默认构造函数初始化,而内置类型的元素初始化为 0。

         map <string, int> word_count; // empty map

    word_count["Anna"] = 1;

将发生以下事情:

         1)在 word_count 中查找键为 Anna 的元素,没有找到。

         2)将一个新的键-值对插入到 word_count 中。它的键是 const string 类型的对象,保存 Anna。而它的值则采用值初始化,这就意味着在本例中值为 0。

         3)将这个新的键-值对插入到 word_count 中。

         4)读取新插入的元素,并将它的值赋为 1。

有别于 vector 或 string 类型,map 下标操作符返回的类型与对 map 迭代器进行解引用获得的类型不相同。map 迭代器返回 value_type 类型的值——包含 const key_type 和 mapped_type 类型成员的 pair 对象;下标操作符则返回一个 mapped_type 类型的值。

下标行为的编程意义:

         // count number of times each word occurs in the input

     map<string, int> word_count; // empty map from string to int

     string word;

     while (cin >> word)

       ++word_count[word];

         这段程序创建一个 map 对象,用来记录每个单词出现的次数。while 循环每次从标准输入读取一个单词。如果这是一个新的单词,则在 word_count 中添加以该单词为索引的新元素。如果读入的单词已在 map 对象中,则将它所对应的值加 1。中最有趣的是,在单词第一次出现时,会在 word_count 中创建并插入一个以该单词为索引的新元素,同时将它的值初始化为 0。然后其值立即加 1,所以每次在 map 中添加新元素时,所统计的出现次数正好从 1 开始。

map::insert 的使用:

         m.insert(e)  e 是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则插入一个值为 e.second 的新元素;如果该键在 m 中已存在,则保持 m 不变。该函数返回一个 pair 类型对象,包含指向键为 e.first 的元素的 map 迭代器,以及一个 bool 类型的对象,表示是否插入了该元素

        m.insert(beg, end) beg 和 end 是标记元素范围的迭代器,其中的元素必须为 m.value_type 类型的键-值对。对于该范围内的所有元素,如果它的键在 m 中不存在,则将该键及其关联的值插入到 m。返回 void 类型

        m.insert(iter, e)  e 是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则创建新元素,并以迭代器 iter 为起点搜索新元素存储的位置。返回一个迭代器,指向 m 中具有给定键的元素

以 insert 代替下标运算:使用下标给 map 容器添加新元素时,元素的值部分将采用值初始化。通常,我们会立即为其赋值,其实就是对同一个对象进行初始化并赋值。而插入元素的另一个方法是:直接使用 insert 成员,其语法更紧凑:

     // if Anna not already in word_count, inserts new element with value 1

     word_count.insert(map<string, int>::value_type("Anna", 1));

         在添加新 map 元素时,使用 insert 成员可避免使用下标操作符所带来的副作用:不必要的初始化。

传递给 insert 的实参相当笨拙。可用两种方法简化:使用 make_pair:

     word_count.insert(make_pair("Anna", 1));

         或使用 typedef

     typedef map<string,int>::value_type valType;

     word_count.insert(valType("Anna", 1));

检测 insert 的返回值: 带有一个键-值 pair 形参的 insert 版本将返回一个值:包含一个迭代器和一个 bool 值的 pair 对象,其中迭代器指向 map 中具有相应键的元素,而 bool 值则表示是否插入了该元素。如果该键已在容器中,则其关联的值保持不变,返回的 bool 值为 true。在这两种情况下,迭代器都将指向具有给定键的元素。含有一个或一对迭代器形参的 insert 函数版本并不说明是否有或有多少个元素插入到容器中。

语法展开: pair<map<string, int>::iterator, bool> ret = word_count.insert(make_pair(word, 1));

         对这个表达式一步步地展开解释:

         1) ret 存储 insert 函数返回的 pair 对象。该 pair 的 first 成员是一个 map 迭代器,指向插入的键。

         2) ret.first 从 insert 返回的 pair 对象中获取 map 迭代器。

         3) ret.first->second 对该迭代器进行解引用,获得一个 value_type 类型的对象。这个对象同样是 pair 类型的,它的 second 成员即为我们所添加的元素的值部分。

         4) ++ret.first->second 实现该值的自增运算。

         归结起来,这个自增语句获取指向按 word 索引的元素的迭代器,并将该元素的值加 1。

查找并读取 map 中的元素:

         下标操作符给出了读取一个值的最简单方法:

     map<string,int> word_count;

     int occurs = word_count["foobar"];

         但是,使用下标存在一个很危险的副作用:如果该键不在 map 容器中,那么下标操作会插入一个具有该键的新元素。

不修改 map 对象的查询操作:

         m.count(k) 返回 m 中 k 的出现次数

        m.find(k) 如果 m 容器中存在按 k 索引的元素,则返回指向该元素的迭代器。如果不存在,则返回超出末端迭代器.

从 map 对象中删除元素:

         m.erase(k) 删除 m 中键为 k 的元素。返回 size_type 类型的值,表示删除的元素个数

        m.erase(p) 从 m 中删除迭代器 p 所指向的元素。p 必须指向 m 中确实存在的元素,而且不能等于 m.end()。返回 void

         m.erase(b, e) 从 m 中删除一段范围内的元素,该范围由迭代器对 b 和 e 标记。b 和 e 必须标记 m 中的一段有效范围:即 b 和 e 都必须指向 m 中的元素或最后一个元素的下一个位置。而且,b 和 e 要么相等(此时删除的范围为空),要么 b 所指向的元素必须出现在 e 所指向的元素之前。返回 void 类型

map 对象的迭代遍历: 在使用迭代器遍历 map 容器时,迭代器指向的元素按键的升序排列。

     map<string, int>::const_iterator map_it = word_count.begin();

     while (map_it != word_count.end()) {

         cout << map_it->first << " occurs "

              << map_it->second << " times" << endl;

         ++map_it; // increment iterator to denote the next element

     }

4. set 类型

set 容器只是单纯的键的集合, 除了两种例外情况,set 容器支持大部分的 map 操作. 两种例外包括:set 不支持下标操作符,而且没有定义 mapped_type 类型。在 set 容器中,value_type 不是 pair 类型,而是与 key_type 相同的类型。它们指的都是 set 中存储的元素类型。

与 map 容器一样,set 容器的每个键都只能对应一个元素。以一段范围的元素初始化 set 对象,或在 set 对象中插入一组元素时,对于每个键,事实上都只添加了一个元素:

     vector<int> ivec;

     for (vector<int>::size_type i = 0; i != 10; ++i) {

         ivec.push_back(i);

         ivec.push_back(i); // duplicate copies of each number

     }

     set<int> iset(ivec.begin(), ivec.end());

     cout << ivec.size() << endl;      // prints 20

     cout << iset.size() << endl;      // prints 10

与 map 容器的操作一样,带有一个键参数的 insert 版本返回 pair 类型对象,包含一个迭代器和一个 bool 值,迭代器指向拥有该键的元素,而 bool 值表明是否添加了元素。使用迭代器对的 insert 版本返回 void 类型。

         set<string> set1;         // empty set

     set1.insert("the");       // set1 now has one element

     set1.insert("and");       // set1 now has two elements

set 容器不提供下标操作符。为了通过键从 set 中获取元素,可使用 find 运算。如果只需简单地判断某个元素是否存在,同样可以使用 count 运算,返回 set 中该键对应的元素个数。

正如不能修改 map 中元素的键部分一样,set 中的键也为 const。在获得指向 set 中某元素的迭代器后,只能对其做读操作,而不能做写操作

5. multimap 和 multiset 类型

multiset 和 multimap 类型则允许一个键对应多个实例。multimap 和 multiset 类型与相应的单元素版本具有相同的头文件定义:分别是 map 和 set 头文件。

multimap 和 multiset 所支持的操作分别与 map 和 set 的操作相同,只有一个例外:multimap 不支持下标运算。为了顺应一个键可以对应多个值这一性质,map 和 multimap,或 set 和 multiset 中相同的操作都以不同的方式做出了一定的修改。在使用 multimap 或 multiset 时,对于某个键,必须做好处理多个值的准备,而非只有单一的值。

由于键不要求是唯一的,因此每次调用 insert 总会添加一个元素。// adds first element with key Barth

     authors.insert(make_pair(

       string("Barth, John"),

       string("Sot-Weed Factor")));

     // ok: adds second element with key Barth

     authors.insert(make_pair(

       string("Barth, John"),

       string("Lost in the Funhouse")));

带有一个键参数的 erase 版本将删除拥有该键的所有元素,并返回删除元素的个数。而带有一个或一对迭代器参数的版本只删除指定的元素,并返回 void 类型.

         multimap<string, string> authors;

     string search_item("Kazuo Ishiguro");

     // erase all elements with this key; returns number of elements removed

     multimap<string, string>::size_type cnt =

                               authors.erase(search_item);

关联容器 map 和 set 的元素是按顺序存储的。而 multimap 和 multset 也一样。因此,在 multimap 和 multiset 容器中,如果某个键对应多个实例,则这些实例在容器中将相邻存放。迭代遍历 multimap 或 multiset 容器时,可保证依次返回特定键所关联的所有元素。

在multimap 或 multiset中查找一个元素的过程较复杂:某键对应的元素可能出现多次。该问题可用三种策略解决。而且三种策略都基于一个事实——在 multimap 中,同一个键所关联的元素必然相邻存放。

         1)使用 find 和 count 操作:count 函数求出某键出现的次数,而 find 操作则返回一个迭代器,指向第一个拥有正在查找的键的实例:

     string search_item("Alain de Botton");

     typedef multimap<string, string>::size_type sz_type;

     sz_type entries = authors.count(search_item);

     multimap<string,string>::iterator iter = authors.find(search_item);

     for (sz_type cnt = 0; cnt != entries; ++cnt, ++iter) cout <<

            iter->second << endl; // print each title

         2)与众不同的面向迭代器的解决方案:另一个更优雅简洁的方法是使用两个未曾见过的关联容器的操作:lower_bound 和 upper_bound。以下这些操作适用于所有的关联容器,也可用于普通的 map 和 set 容器,但更常用于 multimap 和 multiset。所有这些操作都需要传递一个键,并返回一个迭代器:

         m.lower_bound(k) 返回一个迭代器,指向键不小于 k 的第一个元素

         m.upper_bound(k) 返回一个迭代器,指向键大于 k 的第一个元素

        m.equal_range(k)          返回一个迭代器的 pair 对象,它的 first 成员等价于 m.lower_bound(k)。而 second 成员则等价于 m.upper_bound(k)

在同一个键上调用 lower_bound 和 upper_bound,将产生一个迭代器范围,指示出该键所关联的所有元素。如果该键在容器中存在,则会获得两个不同的迭代器:lower_bound 返回的迭代器指向该键关联的第一个实例,而 upper_bound 返回的迭代器则指向最后一个实例的下一位置。如果该键不在 multimap 中,这两个操作将返回同一个迭代器,指向依据元素的排列顺序该键应该插入的位置。这两个操作不会说明键是否存在,其关键之处在于返回值给出了迭代器范围。

这些操作返回的也可能是容器自身的超出末端迭代器。如果所查找的元素拥有 multimap 容器中最大的键,那么的该键上调用 upper_bound 将返回超出末端迭代器。如果所查找的键不存在,而且比 multimap 容器中所有的键都大,则 low_bound 也将返回超出末端迭代器。lower_bound 返回的迭代器不一定指向拥有特定键的元素。如果该键不在容器中,则 lower_bound 返回在保持容器元素顺序的前提下该键应被插入的第一个位置。

     typedef multimap<string, string>::iterator authors_it;
     authors_it beg = authors.lower_bound(search_item),
                end = authors.upper_bound(search_item);
     while (beg != end) {
         cout << beg->second << endl; // print each title
         ++beg;
     }

若该键没有关联的元素,则 lower_bound 和 upper_bound 返回相同的迭代器:都指向同一个元素或同时指向 multimap 的超出末端位置。它们都指向在保持容器元素顺序的前提下该键应被插入的位置。

         3)equal_range 函数返回存储一对迭代器的 pair 对象。如果该值存在,则 pair 对象中的第一个迭代器指向该键关联的第一个实例,第二个迭代器指向该键关联的最后一个实例的下一位置。如果找不到匹配的元素,则 pair 对象中的两个迭代器都将指向此键应该插入的位置。

         pair<authors_it, authors_it> pos = authors.equal_range(search_item);

     while (pos.first != pos.second) {

         cout << pos.first->second << endl; // print each title

         ++pos.first;

     }

6. 容器的综合应用:文本查询程序

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值