C++ Prime 第十章 关联容器

http://blog.163.com/sentimental_man/blog/static/73001618200811235945334/

C++ Prime第十章 关联容器

本章将继续介绍标准库容器类型的另一项内容——关联容器。关联容器和顺序容器的本质差别在于:关联容器通过键(key)存储和读取元素,而顺序容器则通过元素在容器中的位置顺序存储和访问元素。

set 和 map 类型的对象所包含的元素都具有不同的键,不允许为同一个键添加第二个元素。如果一个键必须对应多个实例,则需使用 multimap 或 multi set,这两种类型允许多个元素拥有相同的键

表 10.1. 关联容器类型

map

关联数组:元素通过键来存储和读取

set

大小可变的集合,支持通过键实现的快速读取

multimap

支持同一个键多次出现的 map 类型

multiset

支持同一个键多次出现的 set 类型

pair 类型

在开始介绍关联容器之前,必须先了解一种与之相关的简单的标准库类型——pair表 10.2),该类型在 utility 头文件中定义。

表 10.2 pairs 类型提供的操作

pair<T1, T2> p1;

创建一个空的 pair 对象,它的两个元素分别是 T1 和 T2 类型,采用值初始化(第 3.3.1 节

 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 的(公有)数据成员

pair 类型的使用相当繁琐,因此,如果需要定义多个相同的 pair 类型对象,可考虑利用 typedef 简化其声明:

     typedef pair<string, string> Author;

     Author proust("Marcel", "Proust");

     Author joyce("James", "Joyce");

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

     string firstBook;

     // access and test the data members of the pair

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

         firstBook = "Stephen Hero";


除了构造函数,标准库还定义了一个 make_pair 函数,由传递给它的两个实参生成一个新的 pair 对象。可如下使用该函数创建新的 pair 对象,并赋给已存在的 pair 对象:

     pair<string, string> next_auth;

     string first, last;

     while (cin >> first >> last) {

         // generate a pair from first and last

         next_auth = make_pair(first, last);

         // process next_auth...

     }

这个循环处理一系列的作者信息:在 while 循环条件中读入的作者名字作为实参,调用 make_pair 函数生成一个新的 pair 对象。此操作等价于下面更复杂的操作:

     // use pair constructor to make first and last into a pair

     next_auth = pair<string, string>(first, last);

注意,对于 map 容器,value_type 并非元素的类型,而是描述键及其关联值类型的 pair 类型。

关联容器支持的操作:

表 9.10 列出的 clear 和 erase 操作,但关联容器的 erase 运算返回 void 类型。

表 9.8 列出的关于容器大小的操作。但 resize 函数不能用于关联容器。

表 10.3. map 的构造函数

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 对象的键时,可使用任意一个键值来访问相应的元素。

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

表 10.4. map 类定义的类型

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 的接口时,需谨记 value_type 是 pair 类型,它的值成员可以修改,但键成员不能修改。

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

10.3.3. 给 map 添加元素

10.3.4. 使用下标访问 map 对象

When we write

如下编写程序时:

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

     // insert default initialzed element with key Anna; then assign 1 to its value

     word_count["Anna"] = 1;


 

the following steps take place:

将发生以下事情:

 

1.

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

2.

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

3.

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

4.

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

使用下标访问 map 与使用下标访问数组或 vector 的行为截然不同:用下标访问不存在的元素将导致在 map 容器中添加一个新元素,它的键即为该下标值。

通常来说,下标操作符返回左值。它返回的左值是特定键所关联的值。可如下读或写元素:

     cout << word_count["Anna"]; // fetch element indexed by Anna; prints 1

     ++word_count["Anna"];       // fetch the element and add one to it

     cout << word_count["Anna"]; // fetch the element and print it; prints 2

有别于 vector 或 string 类型,map 下标操作符返回的类型与对 map 迭代器进行解引用获得的类型不相同。

显然,map 迭代器返回 value_type 类型的值——包含 const key_type 和 mapped_type 类型成员的 pair 对象;下标操作符则返回一个 mapped_type 类型的值。

10.3.5. map::insert 的使用

map 容器的 insert 成员与顺序容器的类似,但有一点要注意:必须考虑键的作用。键影响了实参的类型:插入单个元素的 insert 版本使用键-值 pair 类型的参数。类似地,对于参数为一对迭代器的版本,迭代器必须指向键-值 pair 类型的元素。另一个差别则是:map 容器的接受单个值的 insert 版本的返回类型。本节的后续部分将详细阐述这一特性。

表 10.5. map 容器提供的 insert 操作

 

m.insert(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)

是一个用在 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));

这个 insert 函数版本的实参:

     map<string, int>::value_type(anna, 1)

是一个新创建的 pair 对象,将直接插入到 map 容器中。谨记 value_type 是 pair<const K, V> 类型的同义词,K 为键类型,而 V 是键所关联的值的类型。insert 的实参创建了一个适当的 pair 类型新对象,该对象将插入到 map 容器。在添加新 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 的返回值

There can be only one element with a given key in a map. If we attempt to insert an element with a key that is already in the map, then insert does nothing. The versions of insert that take an iterator or iterator pair do not indicate whether or how many elements were inserted.

map 对象中一个给定键只对应一个元素。如果试图插入的元素所对应的键已在容器中,则 insert 将不做任何操作。含有一个或一对迭代器形参的 insert 函数版本并不说明是否有或有多少个元素插入到容器中。

但是,带有一个键-值 pair 形参的 insert 版本将返回一个值:包含一个迭代器和一个 bool 值的 pair 对象,其中迭代器指向 map 中具有相应键的元素,而 bool 值则表示是否插入了该元素。如果该键已在容器中,则其关联的值保持不变,返回的 bool 值为 false。在这两种情况下,迭代器都将指向具有给定键的元素。下面是使用 insert 重写的单词统计程序:

     // 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) {

         // inserts element with key equal to word and value 1;

         // if word already in word_count, insert does nothing

         pair<map<string, int>::iterator, bool> ret =

                   word_count.insert(make_pair(word, 1));

         if (!ret.second)          // word already in word_count

             ++ret.first->second;  // increment counter

     }

对于每个单词,都尝试 insert 它,并将它的值赋 1。if 语句检测 insert 函数返回值中的 bool 值。如果该值为 false,则表示没有做插入操作,按 word 索引的元素已在 word_count 中存在。此时,将该元素所关联的值加 1。

10.3.6. 查找并读取 map 中的元素

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

     map<string,int> word_count;

     int occurs = word_count["foobar"];

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

这样的行为是否正确取决于程序员的意愿。在这个例子中,如果“foobar”不存在,则在 map 中插入具有该键的新元素,其关联的值为 0。在这种情况下,occurs 获得 0 值。

我们的单词统计程序的确是要通过下标引用一个不存在的元素来实现新元素的插入,并将其关联的值初始化为 0。然而,大多数情况下,我们只想知道某元素是否存在,而当该元素不存在时,并不想做做插入运算。对于这种应用,则不能使用下标操作符来判断元素是否存在。

map 容器提供了两个操作:count 和 find,用于检查某个键是否存在而不会插入该键。

表 10.6. 不修改 map 对象的查询操作

m.count(k)

返回 m 中 k 的出现次数

m.find(k)

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

对于 map 对象,count 成员的返回值只能是 0 或 1。map 容器只允许一个键对应一个实例,所以 count 可有效地表明一个键是否存在。而对于 multimaps 容器,count 的返回值将有更多的用途,相关内容将会在第 10.5 节中介绍。如果返回值非 0,则可以使用下标操作符来获取该键所关联的值,而不必担心这样做会在 map 中插入新元素:

     int occurs = 0;

     if (word_count.count("foobar"))

         occurs = word_count["foobar"];

当然,在执行 count 后再使用下标操作符,实际上是对元素作了两次查找。如果希望当元素存在时就使用它,则应该用 find 操作。

读取元素而不插入该元素

find 操作返回指向元素的迭代器,如果元素不存在,则返回 end 迭代器:

     int occurs = 0;

     map<string,int>::iterator it = word_count.find("foobar");

     if (it != word_count.end())

         occurs = it->second;

如果希望当具有指定键的元素存在时,就获取该元素的引用,否则就不在容器中创建新元素,那么应该使用 find。

10.3.7. 从 map 对象中删除元素

从 map 容器中删除元素的 erase 操作有三种变化形式(表 10.7)。与顺序容器一样,可向 erase 传递一个或一对迭代器,来删除单个元素或一段范围内的元素。其删除功能类似于顺序容器,但有一点不同:map 容器的 erase 操作返回 void,而顺序容器的 erase 操作则返回一个迭代器,指向被删除元素后面的元素。

除此之外,map 类型还提供了一种额外的 erase 操作,其参数是 key_type 类型的值,如果拥有该键的元素存在,则删除该元素。对于单词统计程序,可使用这个版本的 erase 函数来删除 word_count 中指定的单词,然后输出被删除的单词:

     // erase of a key returns number of elements removed

     if (word_count.erase(removal_word))

          cout << "ok: " << removal_word << " removed\n";

     else cout << "oops: " << removal_word << " not found!\n";

erase 函数返回被删除元素的个数。对于 map 容器,该值必然是 0 或 1。如果返回 0,则表示欲删除的元素在 map 不存在

表 10.7. 从 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 容器时,迭代器指向的元素按键的升序排列。

set 类型

除了两种例外情况,set 容器支持大部分的 map 操作.

两种例外包括:set 不支持下标操作符,而且没有定义 mapped_type 类型。在 set 容器中,value_type 不是 pair 类型,而是与 key_type 相同的类型。它们指的都是 set 中存储的元素类型。这一差别也体现了 set 存储的元素仅仅是键,而没有所关联的值。与 map 一样,set 容器存储的键也必须唯一,而且不能修改。

从 set 中获取元素

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

     iset.find(1)     // returns iterator that refers to the element with key == 1

     iset.find(11)    // returns iterator == iset.end()

     iset.count(1)    // returns 1

     iset.count(11)   // returns 0

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

     // set_it refers to the element with key == 1

     set<int>::iterator set_it = iset.find(1);

     *set_it = 11;               // error: keys in a set are read-only

     cout << *set_it << endl;    // ok: can read the key

10.5. multimap 和 multiset 类型

10.5.1. 元素的添加和删除

The insert operations described in Table 10.5 (p. 365) and the erase operations described in Table 10.7 (p. 369) are used to add and remove elements of a multimap or multiset.

表 10.5 描述的 insert 操作和表 10.7 描述的 erase 操作同样适用于 multimap 以及 multiset 容器,实现元素的添加和删除。

由于键不要求是唯一的,因此每次调用 insert 总会添加一个元素。例如,可如下定义一个 multimap 容器对象将作者映射到他们所写的书的书名上。这样的映射可为一个作者存储多个条目:

     // 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);

10.5.2. 在 multimap 和 multiset 中查找元素

注意到,关联容器 map 和 set 的元素是按顺序存储的。而 multimap 和 multset 也一样。因此,在 multimap 和 multiset 容器中,如果某个键对应多个实例,则这些实例在容器中将相邻存放。

迭代遍历 multimap 或 multiset 容器时,可保证依次返回特定键所关联的所有元素

使用 find 和 count 操作

使用 find 和 count 可有效地解决刚才的问题。count 函数求出某键出现的次数,而 find 操作则返回一个迭代器,指向第一个拥有正在查找的键的实例:

     // author we'll look for

     string search_item("Alain de Botton");

     // how many entries are there for this author

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

     sz_type entries = authors.count(search_item);

     // get iterator to the first entry for this author

     multimap<string,string>::iterator iter =

                              authors.find(search_item);

     // loop through the number of entries there are for this author

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

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

首先,调用 count 确定某作者所写的书籍数目,然后调用 find 获得指向第一个该键所关联的元素的迭代器。for 循环迭代的次数依赖于 count 返回的值。在特殊情况下,如果 count 返回 0 值,则该循环永不执行。

首先,调用 count 确定某作者所写的书籍数目,然后调用 find 获得指向第一个该键所关联的元素的迭代器。for 循环迭代的次数依赖于 count 返回的值。在特殊情况下,如果 count 返回 0 值,则该循环永不执行。

与众不同的面向迭代器的解决方案

Another, more elegant strategy uses two associative container operations that we haven't seen yet: lower_bound and upper_bound. These operations, listed in Table 10.8 (p. 379), apply to all associative containers. They can be used with (plain) maps or sets but are most often used with multimaps or multisets. Each of these operations takes a key and returns an iterator.

另一个更优雅简洁的方法是使用两个未曾见过的关联容器的操作:lower_bound 和 upper_bound。表 10.8 列出的这些操作适用于所有的关联容器,也可用于普通的 map 和 set 容器,但更常用于 multimap 和 multiset。所有这些操作都需要传递一个键,并返回一个迭代器。

 

Table 10.8. Associative Container Operations Returning Iterators

表 10.8. 返回迭代器的关联容器操作

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,将产生一个迭代器范围(第 9.2.1 节),指示出该键所关联的所有元素。如果该键在容器中存在,则会获得两个不同的迭代器:lower_bound 返回的迭代器指向该键关联的第一个实例,而 upper_bound 返回的迭代器则指向最后一个实例的下一位置。如果该键不在 multimap 中,这两个操作将返回同一个迭代器,指向依据元素的排列顺序该键应该插入的位置。

lower_bound 返回的迭代器不一定指向拥有特定键的元素。如果该键不在容器中,则 lower_bound 返回在保持容器元素顺序的前提下该键应被插入的第一个位置。

使用这些操作,可如下重写程序:

     // definitions of authors and search_item as above

     // beg and end denote range of elements for this author

     typedef multimap<string, string>::iterator authors_it;

     authors_it beg = authors.lower_bound(search_item),

                end = authors.upper_bound(search_item);

     // loop through the number of entries there are for this author

     while (beg != end) {

         cout << beg->second << endl; // print each title

         ++beg;

     }

enual_range 函数

事实上,解决上述问题更直接的方法是:调用 equal_range 函数来取代调用 upper_bound 和 lower_bound 函数。equal_range 函数返回存储一对迭代器的 pair 对象。如果该值存在,则 pair 对象中的第一个迭代器指向该键关联的第一个实例,第二个迭代器指向该键关联的最后一个实例的下一位置。如果找不到匹配的元素,则 pair 对象中的两个迭代器都将指向此键应该插入的位置。

事实上,解决上述问题更直接的方法是:调用 equal_range 函数来取代调用 upper_bound 和 lower_bound 函数。equal_range 函数返回存储一对迭代器的 pair 对象。如果该值存在,则 pair 对象中的第一个迭代器指向该键关联的第一个实例,第二个迭代器指向该键关联的最后一个实例的下一位置。如果找不到匹配的元素,则 pair 对象中的两个迭代器都将指向此键应该插入的位置。

We could use equal_range to modify our program once again:

使用 equal_range 函数再次修改程序:

     // definitions of authors and search_item as above

     // pos holds iterators that denote range of elements for this key

     pair<authors_it, authors_it>

                      pos = authors.equal_range(search_item);

     // loop through the number of entries there are for this author

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

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

         ++pos.first;

     }


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值