首页 > 代码库 > C++primer第十章 关联容器

C++primer第十章 关联容器

 关联容器和顺序容器的本质差别在于:关联容器通过键(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 c1C<T> c1(c2); // copies elements from c2 into c1// b and e are iterators denoting a sequenceC<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_countmap<string, int>::iterator map_it = word_count.begin();// *map_it is a reference to a pair<const string, int> objectcout << map_it->first; // prints the key for this elementcout << " " << map_it->second; // prints the value of the elementmap_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 valueword_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 inputmap<string, int> word_count; // empty map from string to intstring 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 value1word_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 intstring 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 removedif (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 elementmap<string, int>::const_iteratormap_it = word_count.begin();// for each element in the mapwhile (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 transformationfile* 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 9vector<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 ivecset<int> iset(ivec.begin(), ivec.end());cout << ivec.size() << endl; // prints 20cout << iset.size() << endl; // prints 10

在 set 中添加元素

set<string> set1; // empty setset1.insert("the"); // set1 now has one elementset1.insert("and"); // set1 now has two elements

另一种方法

set<int> iset2; // empty setiset2.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 Barthauthors.insert(make_pair(string("Barth, John"),string("Sot-Weed Factor")));// ok: adds second element with key Barthauthors.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 removedmultimap<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 forstring search_item("Alain de Botton");// how many entries are there for this authortypedef multimap<string, string>::size_type sz_type;sz_type entries = authors.count(search_item);// get iterator to the first entry for this authormultimap<string,string>::iterator iter =authors.find(search_item);// loop through the number of entries there are for this authorfor (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 authortypedef 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 authorwhile (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 keypair<authors_it, authors_it>pos = authors.equal_range(search_item);// loop through the number of entries there are for this authorwhile (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 queryint 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;}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

C++primer第十章 关联容器