关联容器和顺序容器的本质差别在于:关联容器通过键(key)存储和读取元素,而顺序容器则通过元素在容器中的位置顺序存储和访问元素。
一般来说,如果希望有效地存储不同值的集合,那么使用 set 容器比较合适,而 map 容器则更适用于需要存储(乃至修改)每个键所关联的值的情况。在做某种文本处理时,可使用 set 保存要忽略的单词。而字典则是 map 的一种很好的应用:单词本身是键,而它的解释说明则是值。
set 和 map 类型的对象所包含的元素都具有不同的键,不允许为同一个键添加第二个元素。如果一个键必须对应多个实例,则需使用 multimap 或 multiset,这两种类型允许多个元素拥有相同的键。
10.1. 引言:pair 类型
该类型在 utility 头文件中定义。
10.2. 关联容器
关联容器共享大部分——但并非全部——的顺序容器操作。关联容器不提供front、 push_front、 pop_front、back、push_back 以及 pop_back 操作。
顺序容器和关联容器公共的操作包括下面的几种:
• 表 9.2 描述的前三种构造函数:
C<T> c; // creates an empty container // c2 must be same type as c1 C<T> c1(c2); // copies elements from c2 into c1 // b and e are iterators denoting a sequence C<T> c(b, e); // copies elements from the sequence into c
关联容器不能通过容器大小来定义,因为这样的话就无法知道键所对应的值是什么。
• 第 9.3.4 节中描述的关系运算。
• 表 9.6 列出的 begin、end、rbegin 和 rend 操作。
• 表 9.5 列出的类型别名(typedef)。注意,对于 map 容器,value_type并非元素的类型,而是描述键及其关联值类型的 pair 类型。第 10.3.2节 将详细解释 map 中的类型别名。
• 表 9.11 中描述的 swap 和赋值操作。但关联容器不提供 assign 函数。
• 表 9.10 列出的 clear 和 erase 操作,但关联容器的 erase 运算返回void 类型。
• 表 9.8 列出的关于容器大小的操作。但 resize 函数不能用于关联容器。
根据键排列元素
“容器元素根据键的次序排列”这一事实就是一个重要的结论:在迭代遍历关联容器时,我们可确保按键的顺序的访问元素,而与元素在容器中的存放位置完全无关。
10.3. map 类型
map 类型通常可理解为关联数组(associativearray):可使用键作为下标来获取一个值,正如内置数组类型一样。
10.3.1. map 对象的定义
要使用 map 对象,则必须包含 map 头文件。在定义 map 对象时,必须分别指明键和值的类型(value type)(
map<string, int> word_count; // empty map from string to int
键类型的约束
在使用关联容器时,它的键不但有一个类型,而且还有一个相关的比较函数。
所用的比较函数必须在键类型上定义严格弱排序(strict weak ordering)。
10.3.2. map 定义的类型
map 对象的元素是键-值对
value_type 是存储元素的键以及值的 pair 类型,而且键为 const。
map 迭代器进行解引用将产生 pair 类型的对象
// get an iterator to an element in word_count map<string, int>::iterator map_it = word_count.begin(); // *map_it is a reference to a pair<const string, int> object 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
对迭代器进行解引用将获得一个 pair 对象,它的 first 成员存放键,为const,而 second 成员则存放值。
10.3.3. 给 map 添加元素
定义了 map 容器后,下一步工作就是在容器中添加键-值元素对。该项工作可使用 insert 成员实现;或者,先用下标操作符获取元素,然后给获取的元素赋值。
10.3.4. 使用下标访问 map 对象
map <string, int> word_count; // empty map // insert default initialzed element with key Anna; then assign 1to its value word_count["Anna"] = 1;
将发生以下事情:
1. 在 word_count 中查找键为 Anna 的元素,没有找到。
2. 将一个新的键-值对插入到 word_count 中。它的键是 const string 类型的对象,保存 Anna。而它的值则采用值初始化,这就意味着在本例中值为 0。
3. 将这个新的键-值对插入到 word_count 中。
4. 读取新插入的元素,并将它的值赋为 1。使用下标访问 map 与使用下标访问数组或 vector 的行为截然不同:用下标访问不存在的元素将导致在 map 容器中添加一个新元素,它的键即为该下标值。
使用下标访问 map 与使用下标访问数组或 vector 的行为截然不同:用下标访问不存在的元素将导致在 map 容器中添加一个新元素,它的键即为该下标值。
下标操作符返回值的使用
有别于 vector 或 string 类型,map 下标操作符返回的类型与对 map 迭代器进行解引用获得的类型不相同。
下标行为的编程意义
// 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];
10.3.5. map::insert 的使用
键影响了实参的类型:插入单个元素的 insert 版本使用键-值 pair类型的参数。
使用下标给 map 容器添加新元素时,元素的值部分将采用值初始化。通常,我们会立即为其赋值,其实就是对同一个对象进行初始化并赋值。而插入元素的另一个方法是:直接使用 insert 成员,其语法更紧凑:
// if Anna not already in word_count, inserts new element with value1 word_count.insert(map<string, int>::value_type("Anna", 1));
传递给 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 的返回值
如果试图插入的元素所对应的键已在容器中,则 insert 将不做任何操作。含有一个或一对迭代器形参的 insert函数版本并不说明是否有或有多少个元素插入到容器中。
带有一个键-值 pair 形参的 insert 版本将返回一个值:包含一个迭代器和一个 bool 值的 pair 对象,其中迭代器指向 map 中具有相应键的元素,而 bool 值则表示是否插入了该元素。如果该键已在容器中,则其关联的值保持不变,返回的 bool 值为 true。在这两种情况下,迭代器都将指向具有给定键的元素。
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 }
10.3.6. 查找并读取 map 中的元素
下标操作符给出了读取一个值的最简单方法:
map<string,int> word_count; int occurs = word_count["foobar"];
但是,使用下标存在一个很危险的副作用:如果该键不在 map 容器中,那么下标操作会插入一个具有该键的新元素。
map 容器提供了两个操作:count 和 find,用于检查某个键是否存在而不会插入该键。
m.count(k) | 返回 m 中 k 的出现次数 |
m.find(k) | 如果 m 容器中存在按 k 索引的元素,则返回指向该元素的迭代器。如果不存在,则返回超出末端迭代器(第 3.4 节) |
使用 count 检查 map 对象中某键是否存在
int occurs = 0; if (word_count.count("foobar")) occurs = word_count["foobar"];
当然,在执行 count 后再使用下标操作符,实际上是对元素作了两次查找。如果希望当元素存在时就使用它,则应该用 find 操作。
读取元素而不插入该元素
int occurs = 0; map<string,int>::iterator it = word_count.find("foobar"); if (it != word_count.end()) occurs = it->second;
10.3.7. 从 map 对象中删除元素
与顺序容器一样,可向 erase 传递一个或一对迭代器,来删除单个元素或一段范围内的元素。其删除功能类似于顺序容器,但有一点不同:map 容器的 erase 操作返回 void,而顺序容器的 erase 操作则返回一个迭代器,指向被删除元素后面的元素。
除此之外,map 类型还提供了一种额外的 erase 操作,其参数是 key_type类型的值,如果拥有该键的元素存在,则删除该元素。
// 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";
10.3.8. map 对象的迭代遍历
// get iterator positioned on the first element map<string, int>::const_iterator map_it = word_count.begin(); // for each element in the map while (map_it != word_count.end()) { // print the element key, value pairs cout << map_it->first << " occurs " << map_it->second << " times" << endl; ++map_it; // increment iterator to denote the next element }
10.3.9. “单词转换” map 对象
下面的程序说明如何创建、查找和迭代遍历一个 map 对象.
这个程序求解的问题是:给出一个 string 对象,把它转换为另一个 string 对象。本程序的输入是两个文件。第一个文件包括了若干单词对,每对的第一个单词将出现在输入的字符串中,而第二个单词则是用于输出。本质上,这个文件提供的是单词转换的集合——在遇到第一个单词时,应该将之替换为第二个单词。第二个文件则提供了需要转换的文本。如果单词转换文件的内容是:
‘em them
cuz because
gratz grateful
i I
nah no
pos supposed
sez said
tanx thanks
wuz was
而要转换的文本是:
nah i sez tanx cuz i wuz pos to
not cuz i wuz gratz
则程序将产生如下输出结果:
no I said thanks because I was supposed to
not because I was grateful
单词转换程序
/* * A program to transform words. * Takes two arguments: The first is name of the word transformation file * The second is name of the input to transform */ int main(int argc, char **argv) { // map to hold the word transformation pairs: // key is the word to look for in the input; value is word to use in the output map<string, string> trans_map; string key, value; if (argc != 3) throw runtime_error("wrongnumber of arguments"); // open transformation file and check that open succeeded ifstream map_file; if (!open_file(map_file, argv[1])) throw runtime_error("no transformation file"); // read the transformation map and build the map while (map_file >> key >> value) trans_map.insert(make_pair(key, value)); // ok, now we‘re ready to do the transformations // open the input file and check that the open succeeded ifstream input; if (!open_file(input, argv[2])) throw runtime_error("no input file"); string line; // hold each line from the input // read the text to transform it a line at a time while (getline(input, line)) { istringstream stream(line); // read the line a word at atime string word; bool firstword = true; // controls whether a space is printed while (stream >> word) { // ok: the actual mapwork, this part is the heart of the program map<string, string>::const_iterator map_it = trans_map.find(word); // if this word is in the transformation map if (map_it != trans_map.end()) // replace it by the transformation value in the map word = map_it->second; if (firstword) firstword = false; else cout << " "; // print space between words cout << word; } cout << endl; // done with this line of input } return 0; }
10.4. set 类型
map 容器是键-值对的集合,好比以人名为键的地址和电话号码。相反地,set 容器只是单纯的键的集合
• 第 10.2 节列出的所有通用的容器操作。
• 表 10.3 描述的构造函数。
• 表 10.5 描述的 insert 操作。
• 表 10.6 描述的 count 和 find 操作。
• 表 10.7 描述的 erase 操作。
10.4.1. set 容器的定义和使用
为了使用 set 容器,必须包含 set 头文件。set 支持的操作基本上与 map提供的相同。
// define a vector with 20 elements, holding two copies of each number //from 0 to 9 vector<int> ivec; for (vector<int>::size_type i = 0; i != 10; ++i) { ivec.push_back(i); // duplicate copies of each number } // iset holds unique elements from ivec set<int> iset(ivec.begin(), ivec.end()); cout << ivec.size() << endl; // prints 20 cout << iset.size() << endl; // prints 10
在 set 中添加元素
set<string> set1; // empty set set1.insert("the"); // set1 now has one element set1.insert("and"); // set1 now has two elements
另一种方法
set<int> iset2; // empty set iset2.insert(ivec.begin(), ivec.end()); // iset2 has 10elements
从 set 中获取元素
set 容器不提供下标操作符。为了通过键从 set 中获取元素,可使用 find运算。如果只需简单地判断某个元素是否存在,同样可以使用 count 运算,返回 set 中该键对应的元素个数。当然,对于 set 容器,count 的返回值只能是1(该元素存在)或 0(该元素不存在)
10.4.2. 创建“单词排除”集
从 map 对象 word_count 中删除一个指定的单词。可将这个操作扩展为删除指定文件中所有的单词(即该文件记录的是排除集)。也即,我们的单词统计程序只对那些不在排除集中的单词进行统计。
void restricted_wc(ifstream &remove_file, map<string, int> &word_count) { set<string> excluded; // set to hold words we‘ll ignore string remove_word; while (remove_file >> remove_word) excluded.insert(remove_word); // read input and keep a count for words that aren‘t in theexclusion set string word; while (cin >> word) // increment counter only if the word is not in excluded if (!excluded.count(word)) ++word_count[word]; }
10.5. multimap 和 multiset 类型
map 和 set 容器中,一个键只能对应一个实例。而 multiset 和 multimap类型则允许一个键对应多个实例。例如,在电话簿中,每个人可能有单独的电话号码列表。在作者的文章集中,每位作者可能有单独的文章标题列表。
multimap和 multiset 类型与相应的单元素版本具有相同的头文件定义:分别是 map 和set 头文件。
multimap 和 multiset 所支持的操作分别与 map 和 set 的操作相同,只有一个例外:multimap 不支持下标运算
10.5.1. 元素的添加和删除
由于键不要求是唯一的,因此每次调用 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 版本将删除拥有该键的所有元素,并返回删除元素的个数
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 中查找元素
在 multimap 中,同一个键所关联的元素必然相邻存放
使用 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
与众不同的面向迭代器的解决方案
另一个更优雅简洁的方法是使用两个未曾见过的关联容器的操作:lower_bound 和 upper_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 对象中的两个迭代器都将指向此键应该插入的位置。
// 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; }
10.6. 容器的综合应用:文本查询程序
我们的程序将读取用户指定的任意文本文件,然后允许用户从该文件中查找单词。查询的结果是该单词出现的次数,并列出每次出现所在的行。如果某单词在同一行中多次出现,程序将只显示该行一次。行号按升序显示,即第 7 行应该在第 9 行之前输出,依此类推。
10.6.1. 查询程序的设计
设计程序的一个良好习惯是首先将程序所涉及的操作列出来。明确需要提供的操作有助于建立需要的数据结构和实现这些行为。从需求出发,我们的程序需要支持如下任务:
1. 它必须允许用户指明要处理的文件名字。程序将存储该文件的内容,以便输出每个单词所在的原始行。
2. 它必须将每一行分解为各个单词,并记录每个单词所在的所有行。在输出行号时,应保证以升序输出,并且不重复。
3. 对特定单词的查询将返回出现该单词的所有行的行号。
4. 输出某单词所在的行文本时,程序必须能根据给定的行号从输入文件中获取相应的行
数据结构
我们将用一个简单的类 TextQuery 实现这个程序。再加几种容器的配合使用,就可相当巧妙地满足上述要求。
1. 使用一个 vector<string> 类型的对象存储整个输入文件的副本。输入文件的每一行是该 vector 对象的一个元素。因而,在希望输出某一行时,只需以行号为下标获取该行所在的元素即可。
2. 将每个单词所在的行号存储在一个 set 容器对象中。使用 set 就可确保每行只有一个条目,而且行号将自动按升序排列。
3. 使用一个 map 容器将每个单词与一个 set 容器对象关联起来,该 set容器对象记录此单词所在的行号。
综上所述,我们定义的 TextQuery 类将有两个数据成员:储存输入文件的vector 对象,以及一个 map 容器对象,该对象关联每个输入的单词以及记录该单词所在行号的 set 容器对象。
操作
对于类还要求有良好的接口。然而,一个重要的设计策略首先要确定:查询函数需返回存储一组行号的 set 对象。这个返回类型应该如何设计呢?
事实上,查询的过程相当简单:使用下标访问 map 对象获取关联的 set 对象即可。唯一的问题是如何返回所找到的 set 对象。安全的设计方案是返回该set 对象的副本。但如此一来,就意味着要复制 set 中的每个元素。如果处理的是一个相当庞大的文件,则复制 set 对象的代价会非常昂贵。其他可行的方法包括:返回一个 pair 对象,存储一对指向 set 中元素的迭代器;或者返回set 对象的 const 引用。为简单起见,我们在这里采用返回副本的方法,但注意:如果在实际应用中复制代价太大,需要新考虑其实现方法。
第一、第三和第四个任务是使用这个类的程序员将执行的动作。第二个任务则是类的内部任务。将这四任务映射为类的成员函数,则类的接口需提供下列三个 public 函数:
• read_file 成员函数,其形参为一个 ifstream& 类型对象。该函数每次从文件中读入一行,并将它保存在 vector 容器中。输入完毕后,read_file 将创建关联每个单词及其所在行号的 map 容器。
• run_query 成员函数,其形参为一个 string 类型对象,返回一个 set 对象,该 set 对象包含出现该 string 对象的所有行的行号。
• text_line 成员函数,其形参为一个行号,返回输入文本中该行号对应的文本行。
无论 run_query 还是 text_line 都不会修改调用此函数的对象,因此,可将这两个操作定义为 const 成员函数(第 7.7.1 节)。为实现 read_file 功能,还需定义两个private 函数来读取输入文本和创建 map 容器:
• store_file 函数读入文件,并将文件内容存储在 vector 容器对象中。
• build_map 函数将每一行分解为各个单词,创建 map 容器对象,同时记录每个单词出现行号。
10.6.2. TextQuery 类
class TextQuery { public: // typedef to make declarations easier typedef std::vector<std::string>::size_type line_no; /* interface: * read_file builds internal data structures for the given file * run_query finds the given word and returns set of lines on which it appears * text_line returns a requested line from the input file */ void read_file(std::ifstream &is) { store_file(is); build_map(); } std::set<line_no> run_query(const std::string&) const; std::string text_line(line_no) const; private: // utility functions used by read_file void store_file(std::ifstream&); // store input file void build_map(); // associated each word with a set of line numbers // remember the whole input file std::vector<std::string> lines_of_text; // map word to set of the lines on which it occurs std::map< std::string, std::set<line_no> > word_map; };
10.6.3. TextQuery 类的使用
// program takes single argument specifying the file to query int main(int argc, char **argv) { // open the file from which user will query words ifstream infile; if (argc < 2 || !open_file(infile, argv[1])) { cerr << "No input file!" << endl; return EXIT_FAILURE; } TextQuery tq; tq.read_file(infile); // builds query map // iterate with the user: prompt for a word to find and print results // loop indefinitely; the loop exit is inside the while while (true) { cout << "enter word to look for, or q to quit: "; string s; cin >> s; // stop if hit eof on input or a ‘q‘is entered if (!cin || s == "q") break; // get the set of line numbers on which this word appears set<TextQuery::line_no> locs = tq.run_query(s); // print count and all occurrences, if any print_results(locs, s, tq); } return 0; }