当前位置:嗨网首页>书籍在线阅读

24-关联容器

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

16.4.6 关联容器

关联容器(associative container)是对容器概念的另一个改进。关联容器将值与键关联在一起,并使用键来查找值。例如,值可以是表示雇员信息(如姓名、地址、办公室号码、家庭电话和工作电话、健康计划等)的结构,而键可以是唯一的员工编号。为获取雇员信息,程序将使用键查找雇员结构。前面说过,对于容器X,表达式X::value_type通常指出了存储在容器中的值类型。对于关联容器来说,表达式X::key_type指出了键的类型。

关联容器的优点在于,它提供了对元素的快速访问。与序列相似,关联容器也允许插入新元素,但不能指定元素的插入位置。原因是关联容器通常有用于确定数据放置位置的算法,以便能够快速检索信息。

关联容器通常是使用某种树实现的。树是一种数据结构,其根节点链接到一个或两个节点,而这些节点又链接到一个或两个节点,从而形成分支结构。像链表一样,节点使得添加或删除数据项比较简单;但相对于链表,树的查找速度更快。

STL提供了4种关联容器:set、multiset、map和multimap。前两种是在头文件set(以前分别为set.h和multiset.h)中定义的,而后两种是在头文件map(以前分别为map.h和multimap.h)中定义的。

最简单的关联容器是set,其值类型与键相同,键是唯一的,这意味着集合中不会有多个相同的键。确实,对于set来说,值就是键。multiset类似于set,只是可能有多个值的键相同。例如,如果键和值的类型为int,则multiset对象包含的内容可以是1、2、2、2、3、5、7、7。

在map中,值与键的类型不同,键是唯一的,每个键只对应一个值。multimap与map相似,只是一个键可以与多个值相关联。

有关这些类型的信息很多,无法在本章全部列出(但附录G列出了方法),这里只介绍一个使用set的简单例子和一个使用multimap的简单例子。

1.set示例

STL set模拟了多个概念,它是关联集合,可反转,可排序,且键是唯一的,所以不能存储多个相同的值。与vector和list相似,set也使用模板参数来指定要存储的值类型:

set<string> A; // a set of string objects

第二个模板参数是可选的,可用于指示用来对键进行排序的比较函数或对象。默认情况下,将使用模板less< >(稍后将讨论)。老式C++实现可能没有提供默认值,因此必须显式指定模板参数:

set<string, less<string> > A; // older implementation

请看下面的代码:

const int N = 6;
string s1[N] = {"buffoon", "thinkers", "for", "heavy", "can", "for"};
set<string> A(s1, s1 + N); // initialize set A using a range from array
ostream_iterator<string, char> out(cout, " ");
copy(A.begin(), A.end(), out);

与其他容器相似,set也有一个将迭代器区间作为参数的构造函数(参见表16.6)。这提供了一种将集合初始化为数组内容的简单方法。请记住,区间的最后一个元素是超尾,s1 + N指向数组s1尾部后面的一个位置。上述代码片段的输出表明,键是唯一的(字符串“for”在数组中出现了2次,但在集合中只出现1次),且集合被排序:

buffoon can for heavy thinkers

数学为集合定义了一些标准操作,例如,并集包含两个集合合并后的内容。如果两个集合包含相同的值,则这个值将在并集中只出现一次,这是因为键是唯一的。交集包含两个集合都有的元素。两个集合的差是第一个集合减去两个集合都有的元素。

STL提供了支持这些操作的算法。它们是通用函数,而不是方法,因此并非只能用于set对象。然而,所有set对象都自动满足使用这些算法的先决条件,即容器是经过排序的。set_union()函数接受5个迭代器参数。前两个迭代器定义了第一个集合的区间,接下来的两个定义了第二个集合区间,最后一个迭代器是输出迭代器,指出将结果集合复制到什么位置。例如,要显示集合A和B的并集,可以这样做:

set_union(A.begin(), A.end(), B.begin(), B.end(),
           ostream_iterator<string, char> out(cout, " "));

假设要将结果放到集合C中,而不是显示它,则最后一个参数应是一个指向C的迭代器。显而易见的选择是C.begin(),但它不管用,原因有两个。首先,关联集合将键看作常量,所以C.begin()返回的迭代器是常量迭代器,不能用作输出迭代器。不直接使用C.begin()的第二个原因是,与copy()相似,set_union()将覆盖容器中已有的数据,并要求容器有足够的空间容纳新信息。C是空的,不能满足这种要求。但前面讨论的模板insert_iterator可以解决这两个问题。前面说过,它可以将复制转换为插入。另外,它还模拟了输出迭代器概念,可以用它将信息写入容器。因此,可以创建一个匿名insert_iterator,将信息复制给C。前面说过,其构造函数将容器名称和迭代器作为参数:

set_union(A.begin(), A.end(), B.begin(), B.end(),
           insert_iterator<set<string> >(C, C.begin()));

函数set_intersection()和set_difference()分别查找交集和获得两个集合的差,它们的接口与set_union()相同。

两个有用的set方法是lower_bound()和upper_bound()。方法lower_bound()将键作为参数并返回一个迭代器,该迭代器指向集合中第一个不小于键参数的成员。同样,方法upper_bound()将键作为参数,并返回一个迭代器,该迭代器指向集合中第一个大于键参数的成员。例如,如果有一个字符串集合,则可以用这些方法获得一个这样的区间,即包含集合中从“b”到“f”的所有字符串。

因为排序决定了插入的位置,所以这种类包含只指定要插入的信息,而不指定位置的插入方法。例如,如果A和B是字符串集合,则可以这样做:

string s("tennis");
A.insert(s);                      // insert a value
B.insert(A.begin(), A.end()); // insert a range

程序清单16.13演示了集合的这些用途。

程序清单16.13 setops.cpp

// setops.cpp -- some set operations
#include <iostream>
#include <string>
#include <set>
#include <algorithm>
#include <iterator>
int main()
{
    using namespace std;
    const int N = 6;
    string s1[N] = {"buffoon", "thinkers", "for", "heavy", "can", "for"};
    string s2[N] = {"metal", "any", "food", "elegant", "deliver","for"};
    set<string> A(s1, s1 + N);
    set<string> B(s2, s2 + N);
    ostream_iterator<string, char> out(cout, " ");
    cout << "Set A: ";
    copy(A.begin(), A.end(), out);
    cout << endl;
    cout << "Set B: ";
    copy(B.begin(), B.end(), out);
    cout << endl;
    cout << "Union of A and B:\n";
    set_union(A.begin(), A.end(), B.begin(), B.end(), out);
    cout << endl;
    cout << "Intersection of A and B:\n";
    set_intersection(A.begin(), A.end(), B.begin(), B.end(), out);
    cout << endl;
    cout << "Difference of A and B:\n";
    set_difference(A.begin(), A.end(), B.begin(), B.end(), out);
    cout << endl;
    set<string> C;
    cout << "Set C:\n";
    set_union(A.begin(), A.end(), B.begin(), B.end(),
        insert_iterator<set<string> >(C, C.begin()));
    copy(C.begin(), C.end(), out);
    cout << endl;
    string s3("grungy");
    C.insert(s3);
    cout << "Set C after insertion:\n";
    copy(C.begin(), C.end(),out);
    cout << endl;
    cout << "Showing a range:\n";
    copy(C.lower_bound("ghost"),C.upper_bound("spook"), out);
    cout << endl;
    return 0;
}

下面是程序清单16.13中程序的输出:

Set A: buffoon can for heavy thinkers
Set B: any deliver elegant food for metal
Union of A and B:
any buffoon can deliver elegant food for heavy metal thinkers
Intersection of A and B:
for
Difference of A and B:
buffoon can heavy thinkers
Set C:
any buffoon can deliver elegant food for heavy metal thinkers
Set C after insertion:
any buffoon can deliver elegant food for grungy heavy metal thinkers
Showing a range:
grungy heavy metal

和本章中大多数示例一样,程序清单16.13在处理名称空间std时采取了偷懒的方式:

using namespace std;

这样做旨在简化表示方式。这些示例使用了名称空间std中非常多的元素,如果使用using声明或作用域运算符,代码将变得混乱:

std::set<std::string> B(s2, s2 + N);
std::ostream_iterator<std::string, char> out(std::cout, " ");
std::cout << "Set A: ";
std::copy(A.begin(), A.end(), out);

2.multimap示例

与set相似,multimap也是可反转的、经过排序的关联容器,但键和值的类型不同,且同一个键可能与多个值相关联。

基本的multimap声明使用模板参数指定键的类型和存储的值类型。例如,下面的声明创建一个multimap对象,其中键类型为int,存储的值类型为string:

multimap<int,string> codes;

第3个模板参数是可选的,指出用于对键进行排序的比较函数或对象。在默认情况下,将使用模板less< >(稍后将讨论),该模板将键类型作为参数。老式C++实现可能要求显式指定该模板参数。

为将信息结合在一起,实际的值类型将键类型和数据类型结合为一对。为此,STL使用模板类pair<class T, class U>将这两种值存储到一个对象中。如果keytype是键类型,而datatype是存储的数据类型,则值类型为pair<const keytype, datatype>。例如,前面声明的codes对象的值类型为pair<const int, string>。

例如,假设要用区号作为键来存储城市名(这恰好与codes声明一致,它将键类型声明为int,数据类型声明为string),则一种方法是创建一个pair,再将它插入:

pair<const int, string> item(213, "Los Angeles");
codes.insert(item);

也可使用一条语句创建匿名pair对象并将它插入:

codes.insert(pair<const int, string> (213, "Los Angeles"));

因为数据项是按键排序的,所以不需要指出插入位置。

对于pair对象,可以使用first和second成员来访问其两个部分了:

pair<const int, string> item(213, "Los Angeles");
cout << item.first << ' ' << item.second << endl;

如何获得有关multimap对象的信息呢?成员函数count()接受键作为参数,并返回具有该键的元素数目。成员函数lower_bound()和upper_bound()将键作为参数,且工作原理与处理set时相同。成员函数equal_range()用键作为参数,且返回两个迭代器,它们表示的区间与该键匹配。为返回两个值,该方法将它们封装在一个pair对象中,这里pair的两个模板参数都是迭代器。例如,下面的代码打印codes对象中区号为718的所有城市:

pair<multimap<KeyType, string>::iterator,
     multimap<KeyType, string>::iterator> range
                         = codes.equal_range(718);
cout << "Cities with area code 718:\n";
std::multimap<KeyType, std::string>::iterator it;
for (it = range.first; it != range.second; ++it)
     cout << (*it).second << endl;

在声明中可使用C++11自动类型推断功能,这样代码将简化为如下所示:

auto range = codes.equal_range(718);
cout << "Cities with area code 718:\n";
for (auto it = range.first; it != range.second; ++it)
    cout << (*it).second  << endl;

程序清单16.14演示了上述大部分技术,它也使用typedef来简化代码。

程序清单16.14 multimap.cpp

// multmap.cpp -- use a multimap
#include <iostream>
#include <string>
#include <map>
#include <algorithm>
typedef int KeyType;
typedef std::pair<const KeyType, std::string> Pair;
typedef std::multimap<KeyType, std::string> MapCode;
int main()
{
    using namespace std;
    MapCode codes;
    codes.insert(Pair(415, "San Francisco"));
    codes.insert(Pair(510, "Oakland"));
    codes.insert(Pair(718, "Brooklyn"));
    codes.insert(Pair(718, "Staten Island"));
    codes.insert(Pair(415, "San Rafael"));
    codes.insert(Pair(510, "Berkeley"));
    cout << "Number of cities with area code 415: "
         << codes.count(415) << endl;
    cout << "Number of cities with area code 718: "
         << codes.count(718) << endl;
    cout << "Number of cities with area code 510: "
         << codes.count(510) << endl;
    cout << "Area Code City\n";
    MapCode::iterator it;
    for (it = codes.begin(); it != codes.end(); ++it)
        cout << " " << (*it).first << " "
            << (*it).second << endl;
    pair<MapCode::iterator, MapCode::iterator> range
        = codes.equal_range(718);
    cout << "Cities with area code 718:\n";
    for (it = range.first; it != range.second; ++it)
        cout << (*it).second << endl;
    return 0;
}

下面是程序清单16.14中程序的输出:

Number of cities with area code 415: 2
Number of cities with area code 718: 2
Number of cities with area code 510: 2
Area Code City
    415   San Francisco
    415   San Rafael
    510   Oakland
    510   Berkeley
    718   Brooklyn
    718   Staten Island
Cities with area code 718:
Brooklyn
Staten Island