第11 章 关联容器
第11 章 關聯容器
關聯容器和順序容器有著根本的不同:關聯容器中的元素是按關鍵字來保存和訪問的。與之相對,順序容器中的元素是按它們在容器中的位置來順序保存和訪問的。
雖然關聯容器的很多行為與順序容器相同,但其不同之處反映了關鍵字的作用。
關聯容器支持高效的關鍵字查找和訪問。兩個主要的關聯容器(associative-container)類型是 map和set。map 中的元素是一些關鍵字-值(key-value)對:關鍵字起到索引的作用,值則表示與索引相關聯的數據。set 中每個元素只包含一個關鍵字:set 支持高效的關鍵字查詢操作——檢查一個給定關鍵字是否在 set 中。例如,在某些文本處理過程中,可以用一個 set 來保存想要忽略的單詞。字典則是一個很好的使用 map 的例子:可以將單詞作為關鍵字,將單詞釋義作為值。
標準庫提供 8 個關聯容器,如下表所示。
關聯容器類型| map | 關聯數組:保存關鍵字-值對 |
| set | 關鍵字即值,即只保存關鍵字的容器 |
| multimap | 關鍵字可重復出現的 map |
| multiset | 關鍵字可以重復出現的 set |
| 無序集合 | |
| unordered_map | 用哈希函數組織的 map |
| unordered_set | 用哈希函數組織的 set |
| unordered_multimap | 哈希組織的 map:關鍵字可以重復出現 |
| unordered_multiset | 哈希組織的 set:關鍵字可以重復出現 |
11.1 使用關聯容器
map 是關鍵字-值對的集合。例如,可以將一個人的名字作為關鍵字,將其電話號碼作為值。我們稱這樣的數據結構為“將名字映射到電話號碼“。map 類型通常被稱為關聯數組(associative array)。關聯數組與“正常”數組類似,不同之處在于其下標不必是整數。我們通過一個關鍵字而不是位置來查找值。給定一個名字到電話號碼的 map,我們可以使用一個人的名字作為下標來獲取此人的電話號碼。
與之相對,set 就是關鍵字的簡單集合。當只是想知道一個值是否存在時,set 是最有用的。例如,一個企業可以定義一個名為 bad_checks 的 set 來保存那些曾經開過空頭支票的人的名字。在接受一張支票之前,可以查詢 bad_checks 來檢查顧客的名字是否在其中。
使用 map
一個經典的使用關聯數組的例子是單詞計數程序:
// 統計每個單詞在輸入中出現的次數 void count_word() {map<string, int> word_count; // string 到 int 的空 mapstring word;while (cin>>word){++word_count[word]; // 提取 word 的計數器并將其加一}for (const auto &w : word_count)// 對 map 中的每個元素{// 打印結果cout << w.first << " occurs " << w.second<< ((w.second > 1) ? " times" : " time") << endl;} }此程序讀取輸入,報告每個單詞出現多少次。
類似順序容器,關聯容器也是模板。為了定義一個 map,我們必須指定關鍵字和值的類型。在此程序中,map 保存的每個元素中,關鍵字是 string 類型,值是 int 類型。當對 word_count 進行下標操作時,我們使用一個 string 作為下標,獲得與此 string 相關聯的 int 類型的計數器。
使用 set
上一個示例程序的一個合理擴展是:忽略常見單詞,如 “the”、“and”、"or"等。我們可以使用 set 保存想忽略的單詞,只對不出現在集合中的單詞統計出現次數。
// 統計每個單詞在輸入中出現的次數 void count_word1() {map<string, int> word_count; // string 到 int 的空 mapset<string> exclude = { "The","But","And","Or","An","A","the","but","and","or","an","a" };string word;while (cin >> word){// 只統計不在 exclude 中的單詞if (exclude.find(word) == exclude.end()){++word_count[word]; // 獲取 word 的計數器并將其加一} }for (const auto &w : word_count)// 對 map 中的每個元素{// 打印結果cout << w.first << " occurs " << w.second<< ((w.second > 1) ? " times" : " time") << endl;} }與其他容器類似,set 也是模板。為了定義一個 set ,必須指明其元素類型,本例中是 string。與順序容器類似,可以對一個關聯容器的元素進行列表初始化(9.2.4節)。集合 exclude 保存了 12 個我們想忽略的單詞。
此程序與前一個程序的重要不同是,在統計每個單詞出現次數之前,我們檢查單詞是否在忽略集合中,這是在 if 語句中完成的:
if (exclude.find(word) == exclude.end())find 調用返回一個迭代器。如果給定關鍵字在 set 中,迭代器指向該關鍵字。否則,find 返回尾后迭代器。在此程序中,僅當 word 不在 exclude 中時我們才更新 word 的計數器。
11.1 節練習
練習 11.1:描述 map 和 vector 的不同。
兩類容器的根本在于,順序容器中的元素是”順序“ 存儲的(鏈表容器中的元素雖然不是在內存中”連續“存儲的,但仍然是按”順序“存儲的)。理解”順序“ 的關鍵,是理解容器支持的操作形式以及效率。
對于 vector 這樣的順序容器,元素在其中按順序存儲,每個元素有唯一對應的位置編號,所有操作都是按編號(位置)進行的。例如,獲取元素(頭、尾、用下標獲取任意位置)、插入刪除元素(頭、尾、任意位置)、遍歷元素(按元素位置順序逐一訪問)。底層的數據結構是數組、鏈表,簡單但已能保證上述操作的高效。而對于依賴值的元素訪問,例如查找(搜索)給定值(find),在這種數據結構上的實現是要通過遍歷完成的,效率不佳。
而 map 這種關聯容器,就是為了高效實現“按值訪問元素”這類操作而設計的。為了達到這一目的,容器 中的元素是按關鍵字存儲的,關鍵字值與元素數據建立起對應關系,這就是“關聯”的含義。底層數據結構是紅黑數、哈希表等,可高效實現按關鍵字值查找、添加、刪除等操作。
練習 11.2:分別給出最適合使用 list、vector、deque、map 以及 set 的例子。
若元素很小(例如 int ),大致數量預先可知,在程序運行過程中不會劇烈變化。大部分情況下只在末尾添加或刪除需要頻繁訪問任意位置的元素,則 vector 可帶來最高的效率。若需頻繁在頭部和尾部添加或刪除元素,則 deque 是最好的選擇。
如果元素較大(如大的類對象),數量預先不知道,或是程序運行過程中頻繁變化,對元素的訪問更多是順序訪問全部或很多元素,則 list 很適合。
map 很適合對一些對象按它們的某個特征進行訪問的情形。典型的例如按學生的名字來查詢學生信息,即可將學生名字作為關鍵字,將學生信息作為元素值,保存在 map 中。
set,顧名思義,就是集合類型。當需要保存特定的值集合——通常滿足/不滿足某種要求的值集合,用 set 最方便。
// 練習 11.3:編寫你自己的單詞計數程序 void count_word() {map<string, int> word_count; // string 到 int 的空 mapstring word;while (cin >> word){++word_count[word]; // 提取 word 的計數器并將其加一}for (const auto &w : word_count)// 對 map 中的每個元素{// 打印結果cout << w.first << " 出現了 " << w.second<< " 次" << endl;} } // 練習 11.4:擴展你的程序,忽略大小寫和標點。例如,“example."、"example,"和”Example"應該遞增相同的計數器。// 編寫函數 trans,將單詞中的標點去掉,將大寫轉換為小寫 string &trans(string &s) {for (int p = 0; p < s.size(); p++){if (s[p] >= 'A' && s[p] <= 'Z'){s[p] -= ('A' - 'a');}else if (s[p] == ',' || s[p] == '.'){s.erase(p, 1);}}return s; } void count_word1() {map<string, int> word_count; // string 到 int 的空 mapstring word;while (cin >> word){++word_count[trans(word)]; // 獲取 word 的計數器并將其加一}for (const auto &w : word_count)// 對 map 中的每個元素{// 打印結果cout << w.first << " 出現了 " << w.second<< " 次" << endl;} }11.2 關聯容器概述
關聯容器(有序的和無序的)都支持9.2節中介紹的普通容器操作。關聯容器不支持順序容器的位置相關的操作,例如 push_front 或 push_back。原因是關聯容器中元素是根據關鍵字存儲的,這些操作對關聯容器沒有意義。而且,關聯容器也不支持構造函數或插入操作這些接受一個元素值和一個數量值的操作。
除了與順序容器相同的操作外,關聯容器還支持一些順序容器不支持的操作。此外,無序容器還提供一些用來調整哈希性能的操作。
關聯容器的迭代器都是雙向的。
11.2.1 定義關聯容器
如前所示,當定義一個 map 時,必須既指明關鍵字類型又指明值類型;而定義一個 set 時,只需指明關鍵字類型,因為 set 中沒有值。每個關聯容器都定義了一個默認構造函數,它創建一個指定類型的空容器。在新標準下,我們也可以對關聯容器進行值初始化。
map<string, int> words_count; // 空容器// 列表初始化set<string> exclude = { "the","but","or","an","a","The","But","And","Or","An","A"};// 三個元素;autuors 將姓映射為名map<string, string> authors = { {"Joyce","James"},{"Austen","Jane"},{"Dickens","Charles"} };與以往一樣,初始化器必須能轉換為容器中元素的類型。對于 set,元素類型就是關鍵字類型。
當初始化一個 map 時,必須提供關鍵字類型和值類型。我們將每個關鍵字-值對包圍在花括號中:
{key,value}來指出它們一起構成了 map 中的一個元素。在每個花括號中,關鍵字是第一個元素,值是第二個。因此 authors 將姓映射到名,初始化后它包含三個元素。
初始化 multimap 和 multiset
一個 map 或 set 中的關鍵字必須是唯一的,即,對于一個給定的關鍵字,只能有一個元素的關鍵字等于它。容器 multimap 和 multiset 沒有此限制,它們都允許多個元素具有相同的關鍵字。例如,在我們用來統計單詞數量的 map 中,每個單詞只能有一個元素。另一方面,在一個詞典中,一個特定單詞則可具有多個與之關聯的詞義。
下面的例子展示了具有唯一關鍵字的容器與允許重復關鍵字的容器之間的區別。首先,我們將創建一個名為 ivec 的保存 int 的 vector,它包含 20 個元素:0 到 9 每個整數有兩個拷貝。我們將使用此 vector 初始化一個 set 和一個 multiset。
// 定義一個有 20 個元素的 vector,保存 0 到 9 每個整數的兩個拷貝vector<int> ivec;for (vector<int>::size_type i = 0; i != 10; ++i){ivec.push_back(i);ivec.push_back(i); //每個數重復保存一次}// iset 包含來自 ivec 的不重復的元素;miset 包含所有元素 20 個元素set<int> iset(ivec.cbegin(), ivec.cend());multiset<int> miset(ivec.cbegin(), ivec.cend());cout << ivec.size() << endl; // 打印出 20cout << iset.size() << endl; // 打印出 10cout << miset.size() << endl; // 打印出 2011.2.1 節練習
練習 11.5:解釋 map 和 set 的區別。你如何選擇使用哪個?
當需要查找給定值所對應的數據時,應使用 map,其中保存的是 <關鍵字,值>對,按關鍵字訪問值。
如果只需判定給定值是否存在時,應使用 set,它是簡單的值的集合。
練習 11.6:解釋 set 和 list 的差別。你如何選擇使用哪個?
兩者都可以保存元素集合。
如果只需要順序訪問這些元素,或是按位置訪問元素,那么應該使用 list。如果需要快速判斷是否有元素等于給定值,則應使用 set。
// 練習 11.7:定義一個 map,關鍵字是家庭的姓,值是一個 vector,保存家中孩子(們) // 的名。編寫代碼,實現添加新的家庭以及向已有家庭中添加新的孩子。 void add_family(map<string, vector<string>> &families, const string &family) {if (families.find(family) == families.end())families[family] = vector<string>(); } void add_child(map<string, vector<string>> &families, const string &family,const string &child) {families[family].push_back(child); } void test_family() {map<string, vector<string>> families;add_family(families, "張");add_child(families, "張","強");add_child(families, "張", "剛");add_child(families, "王", "五");add_family(families, "王");for (auto f : families){cout << f.first << "家的孩子: ";for (auto c : f.second){cout << c << " ";}cout << endl;} } // 練習 11.8:編寫一個程序,在一個 vector 而不是一個 set 中保存不重復的單詞。使用 set 的優點是什么? string &trans(string &s) {for (int p = 0; p < s.size(); p++){if (s[p] >= 'A' && s[p] <= 'Z')s[p] -= ('A' - 'a');else if (s[p] == ',' || s[p] == '.')s.erase(p, 1);} } // 使用 vector void save_words_toV() {vector<string> unique_word;string word;while (cin >> word){trans(word);if (find(unique_word.begin(), unique_word.end(), word) == unique_word.end())unique_word.push_back(word); // 添加不重復單詞}for (const auto &w : unique_word) // 打印不重復單詞{// 打印結果cout << w << " ";}cout << endl; }// 使用 set void save_words_toSet() {set<string> unique_word;string word;while (cin >> word){trans(word);unique_word.insert(word); // 添加不重復單詞}for (const auto &w : unique_word) // 打印不重復單詞{// 打印結果cout << w << " ";}cout << endl; }使用 set 可以不用編寫檢查重復單詞的代碼。vector 可以保持單詞的輸入順序,而 set 則不能,遍歷 set,元素是按值的升序被遍歷的。
11.2.2 關鍵字類型的要求
關聯容器對其關鍵字類型有一些限制。對于有序容器——map、multimap、set 以及 multiset,關鍵字類型必須定義元素比較的方法。默認情況下,標準庫使用關鍵字類型的 < 運算符來比較兩個關鍵字。在集合類型中,關鍵字類型就是元素類型;在映射類型中,關鍵字類型是元素的第一部分的類型。
傳遞給排序算法的可調用對象必須滿足與關聯容器中關鍵字一樣的類型要求。
有序容器的關鍵字類型
可以向一個算法提供我們自己定義的比較操作(10.3節),與之類似,也可以提供自己定義的操作來代替關鍵字上的 < 運算符。所提供的操作必須在關鍵字類型上定義一個嚴格弱序(strict weak ordering)。可以將嚴格弱序看作“小于等于”。它必須具備如下基本性質:
- 兩個關鍵字不能同時“小于等于”對方;如果 k1 “小于等于” k2,那么 k2 絕不能“小于等于” k1。
- 如果 k1 “小于等于” k2,且 k2 “小于等于” k3,那么 k1 必須 “小于等于” k3。
- 如果存在兩個關鍵字,任何一個都不“小于等于” 另一個,那么我們稱這兩個關鍵字是“等價”的。如果 k1 “等價于” k2,且 k2 “等價于” k3,那么 k1 必須“等價于” k3。
如果兩個關鍵字是等價的(即,任何一個都不“小于等于”另一個),那么容器將它們視作相等來處理。當用作 map 的關鍵字時,只能有一個元素與這兩個關鍵字關聯,我們可以用兩者中任意一個來訪問對應的值。
使用關鍵字類型的比較元素
用來組織一個容器中元素的操作也是該容器類型的一部分。為了指定使用自定義的操作,必須在定義關聯容器類型時提供此操作的類型。如前所述,用尖括號指出要定義哪種類型的容器,自定義的操作類型必須在尖括號中緊跟著元素類型給出。
在尖括號中出現的每個類型,就僅僅是一個類型而已。當我們創建一個容器(對象)時,才會以構造函數參數的形式提供真正的比較操作(其類型必須與在尖括號中指定的類型相吻合)。
例如,我們不能直接定義一個 Sales_data 的 multiset,因為 Sales_data 沒有 < 運算符。但是,可以用 compareIsbn 函數來定義一個 multiset。此函數在 Sales_data 對象的 ISBN 成員上定義了一個嚴格弱序。函數 compareIsbn 應該像這樣定義
bool compareIsbn(const Sales_data &lhs,const Sales_data &rhs) {return lhs.isbn() < rhs.isbn(); }為了使用自己定義的操作,在定義 multiset 時我們必須提供兩個類型:關鍵字類型 Sales_data,以及比較操作類型——應該是一種函數指針類型(6.7節),可以指向 compareIsbn。當定義此容器類型的對象時,需要提供相應使用的操作的指針。在本例中,我們提供一個指向 compareIsbn 的指針
// bookstore 中多條記錄可以有相同的 ISBN // bookstore 中的元素以 ISBN 的順序進行排列 multiset<Sales_data, decltype(compareIsbn)*>bookstore(compareIsbn);此處,我們使用 decltype 來指出自定義操作的類型。記住,當用 decltype 來獲得一個函數指針類型時,必須加上一個 * 來指出我們要使用一個給定函數類型的指針。用 compareIsbn 來初始化 bookstore 對象,這表示當我們向 bookstore 添加元素時,通過調用 compareIsbn 來為這些元素排序。即,bookstore 中的元素將按它們的 ISBN 成員的值排序。可以用 compareIsbn 代替 &compareIsbn 作為構造函數的參數,因為當我們使用一個函數的名字時,在需要的情況下它會自動轉化為一個指針。當然,使用 &compareIsbn 的效果也是一樣的。
11.2.2 節練習
// 練習 11.9:定義一個 map,將單詞與一個行號的 list 關聯,list 中保存的是單詞所出現的行號。 map<string, list<int>>word_lineno; string &trans(string &s) {for (int p = 0; p < s.size(); p++){if (s[p] >= 'A' && s[p] <= 'Z')s[p] -= ('A' - 'a');else if (s[p] == ',' || s[p] == '.')s.erase(p, 1);} } void save_word() {map<string, list<int>>word_lineno; //單詞到行號的映射string line;string word;int lineno = 0;ifstream in;if (!in){cout << "打開輸入文件失敗!" << endl;}while (getline(in, line)) //讀取一行{lineno++; //行號遞增istringstream l_in(line);//構造字符串流,讀取單詞while (l_in >> word){trans(word);word_lineno[word].push_back(lineno); //添加行號}}// 打印單詞行號for (const auto &w : word_lineno){cout << w.first << "所在行: ";for (const auto &i : w.second)cout << i << " ";cout << endl;} }練習 11.10:可以定義一個 vector::iterator 到 int 的 map 嗎?list::iterator 到 int 的 map 呢?對于兩種情況,如果不能,解釋為什么
由于有序容器要求關鍵字類型必須支持比較操作 < ,因此
map<vector<int>::iterator, int> m1;是可以的,因為 vector 的迭代器支持比較操作。而
map<list<int>::iterator,int> m2;是不行的,因為 list 的元素不是連續存儲,其迭代器不支持比較操作。
練習 11.11:不使用 decltype 重新定義 bookstore。
// 首先用typedef 定義與 compareIsbn 相容的函數指針類型,然后用此類型聲明 multiset 即可。 typedef bool (*pf)(const Sales_data &, const Sales_data &) multiset<Sales_data,pf> bookstore(compareIsbn);11.2.3 pair 類型
在介紹關聯容器的操作之前,我們需要了解名為 pair 的標準庫類型,它定義在頭文件 utility 中。
一個 pair 保存兩個數據成員。類似容器,pair 是一個用來生成特定類型的模板。當創建一個 pair 時,我們必須提供兩個類型名,pair 的數據成員將具有對應的類型。兩個類型不要求一樣:
pair<string, string> anon; // 保存兩個 string pair<string, size_t> word_count; // 保存一個 string 和一個 size_t pair<string, vector<int>> line; // 保存 string 和 vector<int>pair 的默認構造函數對數據成員進行值初始化(參見3.3.1節)。因此,anon 是一個包含兩個空 string 的 pair,line 保存一個空 string 和一個空 vector。word_count 中的 size_t 成員值為 0,而 string 成員被初始化為空。
我們也可以為每個成員提供初始化器:
pair<string, string> author{"James","Joyce"};這條語句創建一個名為 author 的 pair ,兩個成員被初始化 為"James"和"Joyce"。
與其他標準庫類型不同,pair 的數據成員是 public 的(7.2節)。兩個分別命名為 first 和 second。我們用普通的額乘以訪問符號(1.5.2節)來訪問它們,例如,在之前的單詞計數程序的輸出語句中我們就是這么做的:
// 打印結果 cout<< w.first << " occurs " << w.second<<((w.second > 1) ? "times" : "time") << endl;此處,w 是指向 map 中某個元素的引用。map 的元素是 pair。下表列出了 pair 上的操作。
pair 上的操作| pair<T1, T2> p; | p 是一個 pair,兩個類型分別為 T1 和 T2 的成員都進行了值初始化(3.3.1節) |
| pair<T1, T2> p (v1, v2); | p 是一個成員類型為 T1 和 T2 的 pair;first 和 second 成員分別用 v1 和 v2 進行初始化 |
| pair<T1, T2> p = {v1, v2}; | 等價于 p(v1, v2) |
| make_pair(v1, v2) | 返回一個用 v1 和 v2 初始化的 pair。pair 的類型從 v1 和 v2 的類型推斷出來 |
| p.first | 返回 p 的名為 first 的(公有)數據成員 |
| p.second | 返回 p 的名為 second 的(公有)數據成員 |
| p1 relop p2 | 關系運算符(<、>、<=、>=)按字典序定義:例如,當 p1.first < p2.first 或 !(p2.first < p1.first) && p1.second < p2.second 成立時,p1 < p2 為 true。關系運算符利用元素的 < 運算符來實現。 |
| p1 == p2 p1 != p2 | 當 first 和 second 成員分別相等時,兩個 pair 相等。相等性判斷利用元素的 == 運算符實現 |
創建 pair 對象的函數
想象有一個函數需要返回一個 pair。在新標準下,我們可以對返回值進行列表初始化(6.3.2節)
// 一個返回 pair 的函數 pair<string,int> process(vector<string> &v) {// 處理 vif (!v.empty()){return { v.back(),v.back().size() }; //列表初始化}else{return pair<string, int>(); // 隱式構造返回值} }若 v 不為空,我們返回一個由 v 中最后一個 string 及其大小組成的 pair。否則,隱式構造一個空 pair,并返回它。
在較早的 C++ 版本中,不允許用花括號包圍的初始化器來返回 pair 這種類型的對象,必須顯式構造返回值:
if (!v.empty()){return pair<string, int>(v.back(),v.back().size()); }我們還可以用 make_pair 來生成 pair 對象,pair 的兩個類型來自于 make_pair 的參數:
if (!v.empty()){return make_pair(v.back(),v.back().size()); }11.2.3 節練習
// 練習 11.12:編寫程序,讀入 string 和 int 的序列,將每個 string 和 int 存入一個 pair 中,pair 保存在一個 vector 中。 void save_to_pair() {string s;int i;vector<pair<string, int>> v;while (cin >> s && cin >> i){v.push_back(pair<string, int>(s, i));;}// 打印數據for (const auto &d : v)cout << d.first << " " << d.second << endl; } // 練習 11.13:編寫上一題程序的三個版本,分別采用不同的方法創建 pair。解釋你認為哪種形式最易于編寫和理解,為什么? void save_to_pair1() {string s;int i;vector<pair<string, int>> v;while (cin >> s && cin >> i){//第一種 默認初始化v.push_back(pair<string, int>(s, i));//第二種 列表初始化v.push_back({ s,i });//第三種v.push_back(make_pair(s, i));}// 打印數據for (const auto &d : v)cout << d.first << " " << d.second << endl; } // 練習 11.14: 擴展 11.2.1 中編寫的孩子姓到名的 map,添加一個 pair 的 vector,保存孩子的名和生日。 void add_family(map<string, vector<pair<string, string>>> &families, const string &family) {families[family]; } void add_child(map<string, vector<pair<string, string>>> &families, const string &family,const string &child,const string &birthday) {families[family].push_back({ child,birthday }); } void test_family() {// vector 中保存 pairmap<string, vector<pair<string, string>>> families;add_family(families, "張");add_child(families, "張","強","2000-1-1");add_child(families, "張", "剛","1980-2-2");add_child(families, "王", "五","1990-1-1");add_family(families, "王");for (auto f : families){cout << f.first << "家的孩子: ";for (auto c : f.second){cout << c.first << " (生日" << c.second << "), ";}cout << endl;} }11.3 關聯容器操作
關聯容器額外的類型別名| key_type | 此容器類型的關鍵字類型 |
| mapped_type | 每個關鍵字關聯的類型;只適用于 map |
| value_type | 對于 set,與 key_value 相同; 對于 map,為 pair<const key_type, mapped_type> |
對于 set 類型,key_type 和 value_type 是一樣的;set中保存的值就是關鍵字。在一個 map 中,元素是關鍵字-值對。即,每個元素是一個 pair 對象,包含一個關鍵字和一個關聯的值。由于我們不能改變一個元素的關鍵字,因此這些 pair 的關鍵字部分是 const 的。
set<string>::value_type v1; // v1 是一個 stringset<string>::key_type v2; // v2 是一個 stringmap<string, int>::value_type v3; // v3 是一個 pair<const string, int>map<string, int>::key_type v4; // v4 是一個 stringmap<string, int>::mapped_type v5;// v5 是一個 int與順序容器一樣(9.2.2節),我們使用作用域運算符來提取一個類型的成員——例如,map<string, int>::key_type。
只有 map 類型(unordered_map、unordered_multimap、multimap 和 map )才定義了 mapped_type。
11.3.1 關聯容器迭代器
當解引用一個關聯容器迭代器時,我們會得到一個類型為容器的 value_type 的值的引用。對 map 而言,value_type 是一個 pair 類型,其 first 成員保存 const 的關鍵字,second 成員保存值:
map<string, int> word_count;// 獲得指向 word_count 中一個元素的迭代器auto map_it = word_count.begin();// *map_it 是指向一個 pair<const string, size_t> 對象的引用cout << map_it->first; // 打印此元素的關鍵字cout << " " << map_it->second; // 打印此元素的值map_it->first = "new key"; // 錯誤:關鍵字是 const 的++map_it->second; // 正確:我們可以通過迭代器改變元素必須記住,一個 map 的 value_type 是一個 pair,我們可以改變 pair 的值,但不能改變關鍵字成員的值。
set 的迭代器是 const 的
雖然 set 類型同時定義了 iterator 和 const_iterator 類型,但兩種類型都只允許只讀訪問 set 中的元素。與不能改變一個 map 元素的關鍵字一樣,一個 set 中的關鍵字也是 const 的。可以用一個 set 迭代器來讀取元素的值,但不能修改:
set<int> iset = { 0,1,2,3,4,5,6,7,8,9 };set<int>::iterator set_it = iset.begin();if (set_it != iset.end()){*set_it = 42; //錯誤: set 中的關鍵字是只讀的cout << *set_it << endl; //正確:可以讀關鍵字}遍歷關聯容器
map 和 set 都支持 begin 和 end 操作。與往常一樣,我們可以用這些函數獲取迭代器,然后用迭代器來遍歷容器。例如,我們可以編寫一個循環來打印單詞計數程序的結果,如下
map<string, int> word_count;// 獲得指向 word_count 中一個元素的迭代器auto map_it = word_count.cbegin();// 比較當前迭代器和尾后迭代器while (map_it != word_count.cend()){// 解引用迭代器,打印關鍵字-值對cout << map_it->first << " occurs "<< map_it->second << "times" << endl;++map_it; //遞增迭代器,移動到下一個元素}本程序的輸出是按字典序排列的。當使用一個迭代器遍歷一個 map、multimap、set 或 multiset 時,迭代器按關鍵字升序遍歷元素。
關聯容器和算法
我們通常不對關聯容器使用泛型算法(參見第 10 章)。關鍵字是 const 這一特性意味著不能將關聯容器傳遞給修改或重排容器元素的算法,因為這類算法需要向元素寫入值,而 set 類型中的元素是 const 的,map 中的元素是 pair,其第一個成員是 const 的。
關聯容器可用于只讀取元素的算法。但是,很多這類算法都有搜索序列。由于關聯容器中的元素不能通過它們的關鍵字進行(快速)查找,因此對其使用泛型搜索算法幾乎總是個壞主意。
在實際編程中,如果我們真要對一個關聯容器使用算法,要么是將它當作一個序列,要么當作一個目的位置。例如,可以用泛型 copy 算法將元素從一個關聯容器拷貝到另一個序列。類似的,可以調用 inserter 將一個插入器綁定(10.4.1節)到一個關聯容器。通過使用 inserter,我們可以將關聯容器當作一個目的位置來調用另一個算法。
11.3.1 節練習
練習 11.15:對一個 int 的 vector 的 map,其 mapped_type、key_type 和 value_type 分別是什么?
vector; int ; pair<const int, vector>.
// 練習 11.16:使用一個 map 迭代器編寫一個表達式,將一個值賦予一個元素。 void map_it() {map<string, int> mp;auto it = mp.begin();it->second = 0; } // 練習 11.17:假定 c 是一個 string 的 multiset,v 是一個 string 的 vector, // 解釋下面的調用。指出每個調用是否合法: void test() {multiset<string> c;vector<string> v;copy(v.begin(), v.end(), inserter(c, c.end()));copy(v.begin(), v.end(), back_inserter(c));copy(c.begin(), c.end(), inserter(v, v.end()));copy(c.begin(), c.end(), back_inserter(v)); }set 的迭代器是 const 的,因此只允許訪問 set 中的元素,而不能改變 set。與 map 一樣,set 的關鍵字也是 const 的,因此也不能通過迭代器來改變 set 中元素的值。
因此,前兩個調用試圖將 vector 中的元素復制到 set 中,是非法的。而后兩個調用將 set 中的元素復制到 vector 中,是合法的。
練習 11.19:定義一個變量,通過對 11.2.2 節中的名為 bookstore 的 multiset 調用 begin() 來初始化這個變量。寫出變量的類型,不要使用 auto 或 decltype。
typedef bool (*pf)(const Sales_data &,const Sales_data &); multiset<Sales_data, pf> bookstore(compareIsbn); ... pair<const Sales_data, pf>::iterator it = bookstore.begin();11.3.2 添加元素
關聯容器的 insert 成員向容器中添加一個元素或一個元素范圍。由于 map 和 set (以及對應的無序類型)包含不重復的關鍵字,因此插入一個已存在的元素對容器沒有任何影響:
vector<int> ivec={2,4,6,8,2,4,6,8};set<int> set2; // 空集合set2.insert(ivec.cbegin(), ivec.cend()); // set2 有 4 個元素cout << endl;set2.insert({ 1,3,5,7,1,3,5,7 }); // set2 現在有 8 個元素insert 有兩個版本,分別接受一對迭代器,或是一個初始化列表,這兩個版本的行為類似對應的構造函數(11.2.1節)——對于一個給定的關鍵字,只有第一個帶此關鍵字的元素才被插入到容器中。
向 map 添加元素
向一個 map 進行 insert 操作時必須記住元素類型是 pair 。通常,對于想要插入的數據,并沒有一個現成的 pair 對象。可以在 insert 的參數列表中創建一個 pair:
map<string, int> word_count;string word;// 向 word_count 插入 word 的 4 種方法word_count.insert({ word,1 });word_count.insert(make_pair(word, 1));word_count.insert(pair<string, int>(word, 1));word_count.insert(map<string, int>::value_type(word, 1));如我們所見,在新標準下,創建一個 pair 最簡單的的方法是在參數列表中使用花括號初始化。也可以調用 make_pair 或顯式構造 pair。最后一個 insert 調用中的參數:
map<string, int>::value_type(s, 1) 關聯容器 insert 操作| c.insert(v) c.emplace(args) | v 是 value_type 類型的對象;args 用來構造一個元素 對于map 和 set ,只有當元素的關鍵字不在 c 中時才插入(或構造)元素。函數返回一個 pair,只包含一個迭代器,指向具有指定關鍵字的元素,以及一個指示是否成功的 bool 值。 對于 multimap 和 multiset ,總會插入(或構造)給定元素,并返回一個指向新元素的迭代器 |
| c.insert(b, e) c.insert(il) | b 和 e 是迭代器,表示一個 c::value_type 類型值的范圍;il 是這種值的花括號列表。函數返回 void 對于 map 和 set,只會插入關鍵字不在 c 中的元素。對于 multimap 和 multiset ,則會插入范圍中的每個元素 |
| c.insert(p, v) c.emplace(p, args) | 類似 insert(v)(或 emplace(args)),但將迭代器 p 作為一個提示,指出從哪里開始搜索新元素應該存儲的位置。返回一個迭代器,指向具有給定關鍵字的元素 |
檢測 insert 的返回值
insert(或 emplace)返回的值依賴于容器類型和參數。對于不包含重復關鍵字的容器,添加單一元素的 insert 和 emplace 版本返回一個 pair,告訴我們插入操作是否成功。pair 的 first 成員是一個迭代器,指向具有給定關鍵字的元素;second 成員是一個 bool 值,指出元素是插入成功還是以及存在于容器中。如果關鍵字已在容器中,則 insert 什么事情也不做,且返回值中的 bool 部分為 false。如果關鍵字不存在,則元素被插入容器中,且 bool 值為 true。
作為一個例子,我們用 insert 重寫單詞計數程序:
// 單詞計數程序 void word_count() {// 統計每個單詞在輸入中出現次數的一種更煩瑣的方法map<string, int>word_count;string word;while (cin >> word){// 插入一個元素,關鍵字等于 word,值為 1;// 若 word 已在 word_count 中,insert 什么也不做auto ret = word_count.insert({ word,1 });if (!ret.second) // word 已在 word_count 中{++ret.first->second; // 遞增計數器}} }對于每個 word,我們嘗試將其插入到容器中,對應的值為 1。若 word 已在 map 中,則什么都不做,特別是與 word 相關聯的計數器的值不變。若 word 還未在 map 中,則此 string 對象被添加到 map 中,且計數器的值被置為 1。
if 語句檢查返回值的 bool 部分,若為 false,則表明插入操作未發生。在此情況下,word 已存在于 word_count 中,因此必須遞增此元素所關聯的計數器。
展開遞增語句
在這個版本的單詞計數程序中,遞增計數器的語句很難理解。通過添加一些括號來反映出運算符的優先級,使表達式更容易理解:
++((ret.first)->second); // 等價表達式下面我們一步一步來解釋此表達式:
ret 保存 insert 返回的值,是一個 pair
ret.first 是 pair 的第一個成員,是一個 map 迭代器,指向具有給定關鍵字的元素
ret.first-> 解引用此迭代器,提取 map 中的元素,元素也是一個 pair
ret.first->second map 中元素的值部分
++ret.first->second 遞增此值
向 multiset 或 multimap 添加元素
我們的單詞計數程序依賴于這樣一個事實:一個給定的關鍵字只能出現一次。這樣任意給定的單詞只有一個關聯的計數器。我們有時希望能添加具有相同關鍵字的多個元素。例如,可能想建立作者到他所著書籍題目的映射。在此情況下,每個作者可能有多個條目,因此我們應該用 multimap 而不是 map。由于一個 multi 容器中的關鍵字不必唯一,在這些類型上調用 insert 總會插入一個元素:
multimap<string, string> authors;// 插入第一個元素,關鍵字為 Barth,Jhonauthors.insert({ "Barth,Jhon","Sot-Weed Factor" });// 正確:添加第二個元素,關鍵字也是 Barth,Jhonauthors.insert({ "Barth,Jhon","Lost in the Funhouse" });for (auto value : authors){cout << value.first << " " << value.second << endl;} }對允許重復關鍵字的容器,接受單個元素的 insert 操作返回一個指向新元素的迭代器。這里無須返回一個 bool 值,因為 insert 總是向這類容器中加入一個新元素。
11.3.2 節練習
練習 11.20:重寫的單詞計數程序,使用 insert 代替下標操作。你認為哪個程序更容易編寫和閱讀?解釋原因。
// 統計每個單詞在輸入中出現的次數 void count_word() {map<string, int> word_count; // string 到 int 的空 mapstring word;while (cin>>word){++word_count[word]; // 提取 word 的計數器并將其加一}for (const auto &w : word_count)// 對 map 中的每個元素{// 打印結果cout << w.first << " occurs " << w.second<< ((w.second > 1) ? " times" : " time") << endl;} }使用下標操作更簡潔易讀
練習 11.21:假定 word_count 是一個 string 到 int 的 map ,word 是一個 string,解釋下面循環的作用:
while (cin >> word) {++word_count.insert({word,0}).first->seond; }循環不斷從標準輸入讀入單詞(字符串),直至遇到文件結束或錯誤。
每讀入一個單詞,構造 pair {word,0},通過 insert 操作插入到 word_count 中。insert 返回一個 pair ,其 first 成員是一個迭代器。若單詞(關鍵字)已存在于容器中,它指向已有元素;否則,它指向新插入的元素。
因此,.first 會得到這個迭代器,指向 word 對應的元素。繼續使用 ->second,可獲得元素的值的引用,即單詞的計數。若單詞是新的,則其值為 0,若已存在,則值是之前出現的次數。對其進行遞增操作,即完成出現次數加 1。
練習 11.22:給定一個 map<string, vector>,對此容器的插入一個元素的 insert 版本,寫出其參數類型和返回類型。
參數類型是一個 pair, first 成員的類型是 map 的關鍵字類型 string,second 成員的類型是 map 的值的類型 vector:
pair<string,vector>
返回類型也是一個 pair,first 成員的類型是 map 的迭代器,second 成員的類型是布爾型:
pair<map<string, vector>::iterator, bool>
// 練習 11.23:11.2.1 節練習中的 map 以孩子的姓為關鍵字,保存他們的名的 vector,用 multimap 重寫此 map。 void add_child(multimap<string, string> &families, const string &family, const string &child) {families.insert({ family,child });} void test_family() {multimap<string, string> families;add_child(families, "張", "強");add_child(families, "張", "剛");add_child(families, "王", "五");for (auto f : families){cout << f.first << "家的孩子: " << f.second << endl;} } i11.3.3 刪除元素
關聯容器定義了三個版本的 erase。如下表所示。與順序容器一樣,我們可以通過傳遞給 erase 一個迭代器或一對迭代器來刪除一個元素或一個元素范圍。這兩個版本的 erase 與對應的順序容器的操作非常相似:指定的元素被刪除,函數返回 void。
關聯容器提供一個額外的 erase 操作,它接受一個 key_type 參數。此版本刪除所有匹配給定關鍵字的元素(如果存在的話),返回實際刪除的元素的數量。我們可以用此版本在打印結果之前從 word_count 中刪除一個特定的單詞:
// 刪除一個關鍵字,返回刪除的元素數量if (word_count.erase(remove_word))cout << "OK: " << remove_word << " removed\n";else cout << "oops: " << remove_word << " not found!\n";對于保存不重復關鍵字的容器,erase 的返回值總是 0 或 1.若返回值為 0,則表明想要刪除的元素并不在容器中。
對允許重復關鍵字的容器,刪除元素的數量可能大于 1:
auto cnt = authors.erase("Barth,Jhon");如果 authors 是 11.3.2 節中創建的 multimap,則 cnt 的值為2。
從關聯容器刪除元素| c.erase(k) | 從 c 中刪除每個關鍵字為 k 的元素。返回一個 size_type 的值,指出刪除的元素的數量 |
| c.erase§ | 從 c 中刪除迭代器 p 指定的元素。p 必須指向 c 中一個真實元素,不能等于 c.end()。返回一個指向 p 之后元素的迭代器,若 p 指向 c 中的尾元素,則返回 c.end() |
| c.erase(b,e) | 刪除迭代器 b 和 e 所表示的范圍中的元素。返回 e |
11.3.4 map 的下標操作
map 和 unordered_map 容器提供了下標運算符和一個對應的 at 函數,如下表所示。set 類型不支持下標,因為 set 中沒有與關鍵字相關聯的“值”。元素本身就是關鍵字,因此“獲取與一個關鍵字相關聯的值”的操作就沒有意義了。我們不能對一個 multimap 或一個 unordered_multimap 進行下標操作,因為這些容器中可能有多個值與一個關鍵字相關聯。
類似我們用過的其他下標運算符,map 下標運算符接受一個索引(即,一個關鍵字),獲取與此關鍵字相關聯的值。但是,與其他下標運算符不同的是,如果關鍵字并不在 map 中,會為它創建一個元素并插入到 map 中,關聯值將進行值初始化(3.3.1 節)。
例如,如果我們編寫如下代碼
map<string, int> word_count; // 插入一個關鍵字為 Anna 的元素,關聯值進行值初始化;然后將 1 賦予它 word_count["Anna"] = 1;將會執行如下操作:
- 在 word_count 中搜索關鍵字為 Anna 的元素,未找到。
- 將一個新的關鍵字-值對插入到 word_count 中。關鍵字是一個 const_string,保存 Anna。值進行值初始化,在本例中意味著值為 0.
- 提取出新插入的元素,并將值 1 賦予它。
由于下標運算符可能插入一個新元素,我們只可以對非 const 的 map 使用下標操作。
對一個 map 使用下標操作很不相同:使用一個不在容器中的關鍵字作為下標,會添加一個具有此關鍵字的元素到 map 中。
map 和 unordered_map 的下標操作| c[k] | 返回關鍵字為 k 的元素;如果 k 不在 c 中,添加一個關鍵字為 k 的元素,對其進行值初始化 |
| c.at(k) | 訪問關鍵字為 k 的元素,帶參數檢查;若 k 不在 c 中,拋出一個 out_of_range 異常 |
使用下標操作的返回值
map 的下標運算符與我們用過的其他下標運算符的另一個不同之處是其返回類型。通常情況下,解引用一個迭代器所返回的類型與下標運算符返回的類型是一樣的。但對 map 則不然:當對一個 map 進行下標操作時,會獲得一個 mapped_type 對象;但當解引用一個 map 迭代器時,會得到一個 value_type 對象(11.3節)。
與其他下標運算符相同的是,map 的下標運算符返回一個左值(4.1.1節)。由于返回的是一個左值,所以我們既可以讀也可以寫元素:
cout<<word_count["Anna"];// 用 Anna 作為下標提取元素;會打印出 1 ++word_count["Anna"]; // 提取元素,將其增1 cout<<word_count["Anna"];//提取元素并打印它;會打印出 211.3.4 節練習
練習 11.24:下面的程序完成什么功能?
map<int, int> m; m[0]=1;若 m 中已有關鍵字 0,下標操作提取出其值,賦值語句將值置為 1。
否則,下標操作會創建一個 pair (0,0),即關鍵字為 0,值為 0(值初始化),將其插入到 m 中,然后提取其值,賦值語句將值置為 1。
練習 11.25:對比下面程序與上一題程序:
vector<int> v; v[0]=1;對于 m,"0"表示“關鍵字 0”。而對于 v,“0” 表示“位置 0”。
若 v 中已有不少于一個元素,即存在“位置 0”元素,則下標操作提取出此位置的元素(左值),賦值操作將其置為 1。而 map 的元素是 pair 類型,下標提取的不是元素,而是元素的第二個成員,即元素的值。
如 v 尚為空,則下標提取出的是一個非法左值(下標操作不做范圍檢查),向其賦值可能導致系統崩潰等嚴重后果。
練習 11.26:可以用什么類型來對一個 map 進行下標操作?下標操作運算符返回的類型是什么?請給出一個具體例子——即,定義一個 map ,然后寫出一個可以用來對 map 進行下標操作的類型以及下標運算符將會返回的類型。
對 map 進行下標操作,應使用其 key_type ,即關鍵字類型。
而下標操作返回的類型是 mapped_type,即關鍵字關聯的值的類型。
比如:map<string, int> 用來進行下標操作的類型是 string,下標操作返回的類型是 int。
11.3.5 訪問元素
關聯容器提供多種查找一個指定元素的方法,如下表所示。如果我們所關心的只不過是一個特定元素是否已在容器中,可能 find 是最佳選擇。對于不允許重復關鍵字的容器,可能使用 find 還是 count 沒什么區別。但是對于運行重復關鍵字的容器,count 還會做更多工作:如果元素在容器中,它還會統計有多少個元素有相同的關鍵字。如果不需要計數,最好使用 find:
set<int> iset = { 0,1,2,3,4,5,6,7,8,9 };iset.find(1); // 返回一個迭代器,指向 key == 1 的元素iset.find(11); // 返回一個迭代器,其值等于 iset.end()iset.count(1); // 返回 1iset.count(0); // 返回 0 在一個關聯容器中查找元素的操作| lower_bound 和 upper_bound 不適用于無序容器 下標 和 at 操作只適用于非 const 的 map 和 unordered_map | |
| c.find(k) | 返回一個迭代器,指向第一個關鍵字為 k 的元素 |
| c.count(k) | 返回關鍵字等于 k 的元素的數量。對于不允許重復關鍵字的容器,返回值永遠是 0 或 1 |
| c.lower_bound(k) | 返回一個迭代器,指向第一個關鍵字不小于 k 的元素 |
| c.upper_bound(k) | 返回一個迭代器,指向第一個關鍵字大于 k 的元素 |
| c.equal_range(k) | 返回一個迭代器 pair,表示關鍵字等于 k 的元素的范圍。若 k 不存在,pair 的兩個成員均等于 c.end() |
對 map 使用 find 代替下標操作
對 map 和 unordered_map 類型,下標運算符提供了最簡單的提取元素的方法。但是,使用下標操作有一個嚴重的副作用:如果關鍵字還未在 map 中,下標操作會插入一個具有給定關鍵字的元素。
但有時,我們只是想知道一個給定元素是否在 map 中,而不想改變 map。在這種情況下,應該使用 find:
if(word_count.find("floor")==word_count.end())cout<<"floor is not in the map"<<endl;在 multimap 或 multiset 中查找元素
對于允許重復關鍵字的容器來說,查找一個元素的過程更為復雜:在容器中可能有很多元素具有給定的關鍵字。如果一個 multimap 或 multiset 中有多個元素具有給定關鍵字,則這些元素在容器中會相鄰存儲。
例如,給定一個作者到著作題目的映射,我們可能想點一個特定作者的所有著作。可以用三種不同的方法來解決這個問題。最直觀的方法是使用 find 和 count:
multimap<string, string> authors;// 插入第一個元素,關鍵字為 Barth,Jhonauthors.insert({ "Barth,Jhon","Sot-Weed Factor" });// 正確:添加第二個元素,關鍵字也是 Barth,Jhonauthors.insert({ "Barth,Jhon","Lost in the Funhouse" }); // 打印一個特定作者的所有著作string search_item("Alain de Botton"); // 要查找的作者auto entries = authors.count(search_item); // 元素的數量auto iter = authors.find(search_item); // 此作者的第一本書// 用一個循環查找此作者的所有著作while (entries){cout << iter->second << endl; // 打印每個題目++iter; // 前進到下一本書--entries; // 記錄已經打印了多少本書}首先調用 count 確定此作者共有多少本著作,并調用 find 獲得一個迭代器,指向第一個關鍵字為此作者的元素。while 循環的迭代次數依賴于 count 的返回值。特別是,如果 count 返回 0,則循環一次也不執行。
一種不同的,面向迭代器的解決方法
我們還可以用 lower_bound 和 upper_bound 來解決此問題。這兩個操作都接受一個關鍵字,返回一個迭代器。如果關鍵字在容器中,lower_bound 返回的迭代器將指向第一個具有給定關鍵字的元素,而 upper_bound 返回的迭代器則指向最后一個匹配給定關鍵字的元素之后的位置。如果元素不在 multimap 中,則 lower_bound 和 upper_bound 會返回相等的迭代器——指向一個不影響排序的關鍵字插入位置。因此,用相同的關鍵字調用 lower_bound 和 upper_bound 會得到一個迭代器范圍,表示所有具有該關鍵字的元素的范圍。
當然,這兩個操作返回的迭代器可能是容器的尾后迭代器。如果我們查找的元素具有容器中最大的關鍵字,則此關鍵字的 upper_bound 返回尾后迭代器。如果關鍵字不存在,且大于容器中任何關鍵字,則 lower_bound 返回的也是尾后迭代器。
使用這兩個操作,我們可以重寫前面的程序:
// 使用 lower_bound 和 upper_bound 重寫前面的程序// beg 和 end 表示對應此作者的元素的范圍for (auto beg = authors.lower_bound(search_item),end = authors.upper_bound(search_item);beg != end; ++beg)cout << beg->second << endl; // 打印每個題目如果 lower_bound 和 upper_bound 返回相同的迭代器,則給定關鍵字不在容器中。
equal_range 函數
解決此問題的最后一種方法是三種方法中最直接的:直接調用 equal_range 即可。此函數接受一個關鍵字,返回一個迭代器 pair。若關鍵字存在,則第一個迭代器指向第一個與關鍵字匹配的元素,第二個迭代器指向最后一個匹配元素之后的位置。若未找到匹配元素,則兩個迭代器都指向關鍵字可以插入的位置。
可以用 equal_range 來再次修改我們的程序:
// pos 保存迭代器對,表示與關鍵字匹配的元素范圍for (auto pos = authors.equal_range(search_item); pos.first != pos.second; ++pos.first)cout << pos.first->second << endl; // 打印每個題目11.3.5 節練習
// 練習 11.28:對一個 string 到 int 的 vector 的 map,定義并初始化一個變量來保存在其上并調用 find 所返回的結果 map<string, vector<int>> m; // 保存 find 返回結果的變量 map<string, vector<int>>::iterator iter;練習 11.29:如果給定的關鍵字不在容器中,upper_bound、lower_bound 和 equal_range 分別會返回什么?
lower_bound 返回第一個具有給定關鍵字的元素,upper_bound 則返回最后一個具有給定關鍵字的元素之后的位置。即,這兩個迭代器構成包含所有具有給定關鍵字的元素的范圍。若給定關鍵字不在容器中,兩個操作顯然應構成一個空范圍,它們返回相當的迭代器,指出關鍵字的正確插入位置——不影響關鍵字的排序。如果給定關鍵字比容器中所有關鍵字都大,則此位置是容器的尾后位置 end 。
equal_range 返回一個 pair,其 first 成員等價于 lower_bound 返回的迭代器,second 成員等價于 upper_bound 返回的迭代器。因此,若給定關鍵字不在容器中,first 和 second 都指向關鍵字的正確插入位置,兩個迭代器構成一個空范圍。
練習 11.30:對于本節最后一個程序中的輸出表達式,解釋運算對象 pos.first->second 的含義。
equal_range 返回一個 pair,其 first 成員與 lower_bound 的返回結果相同,即指向容器中第一個具有給定關鍵字的元素。因此,對其解引用會得到一個 value_type 對象,即一個 pair ,其 first 為元素的關鍵字,即給定關鍵字,而 second 為關鍵字關聯的值。在本例中,關鍵字為作者,關聯的值為著作的題目。因此 pos.first->second 即獲得給定作者的第一部著作的題目。
// 練習 11.31:編寫程序,定義一個作者及其作品的 multimap。使用 find 在 multimap 中查找一個元素并 // erase 刪除它。確保你的程序在元素不在 map 中時也能正常運行。 void remove_author(multimap<string, string> &books, const string &author) {auto pos = books.equal_range(author);if (pos.first == pos.second){cout << "沒有 " << author << " 這個作者" << endl;}else{books.erase(pos.first, pos.second);} } // 練習 11.32:使用上一題定義的 multimap 編寫一個程序,按字典序打印作者列表和他們的作品 void print_books(multimap<string, string> &books) {cout << "當前書目包括: " << endl;for (auto &book : books) // 遍歷所有書籍,打印之cout << book.first << ", 《" << book.second << "》" << endl;cout << endl; }11.3.6 一個單詞轉換的 map
我們將以一個程序結束本節的內容,它將展示 map 的創建、搜索以及遍歷。這個程序的功能是這樣的:給定一個 string,將它轉換為另一個 string。程序的輸入是兩個文件。第一個文件保存的是一些規則,用來轉換第二個文件中的文本。每條規則由兩部分組成:一個可能出現在輸入文件中的單詞和一個用來替換它的短語。表達的含義是,每當第一個單詞出現在輸入中時,我們就將它替換成對應的短語。第二個輸入文件包含要轉換的文本。
如果單詞轉換文件的內容如下所示:
brb be right back k okay? y why r are u you pic picture thk thanks! 18r later我們希望轉換的文本為
where r u y dont u send me a pic k thk 18r則程序應該生成這樣的輸出:
where are you why dont you send me a picture okay? thanks! later單詞轉換程序
我們的程序將使用三個函數。函數 word_transform 管理整個過程。它接受兩個 ifstream 參數:第一個參數綁定到單詞轉換文件,第二個參數應綁定到我們要轉換的文本文件。函數 buildMap會讀取轉換規則文件,并創建一個 map,用于保存每個單詞到其轉換內容的映射。函數transform接受一個 string,如果存在轉換規則,返回轉換后的內容。
我們首先定義 word_transform 函數。最重要的部分是調用 buildMap 和transform:
void word_transform(ifstream &map_file, ifstream &input) {auto trans_map = bulidMap(map_file); // 保存轉換規則string text; // 保存輸入中的每一行while (getline(input, text)) // 讀取一行輸入{istringstream stream(text); // 讀取每個單詞string word;bool firstword = true; // 控制是否打印空格while (stream >> word){if (firstword)firstword = false;elsecout << " "; // 在單詞間打印出一個空格// transform 返回它的第一個參數或其轉換之后的形式cout << transform(word, trans_map); //打印輸出}cout << endl; // 完成一行的轉換} }函數首先調用 buildMap 來生成單詞轉換 map,我們將它保存在 trans_map 中。函數的剩余部分處理輸入文件。while 循環用 getline 一行一行地讀取文件。這樣做的目的是使得輸出中的換行位置能和輸入文件中一樣。為了從每行中獲取單詞,我們使用了一個嵌套的 while 循環,他用一個 istringstream (8.3節) 來處理當前行中的每個單詞。
在輸出過程中,內層 while 循環使用一個 bool 變量 firstword 來確定是否打印一個空格。它通過調用 transform 來獲得要打印的單詞。transform 的返回值或者是 word 中原來的 string,或者是 trans_map 中指出的對應的轉換內容。
建立轉換映射
函數 buildMap 讀入給定文件,建立起轉換映射。
map<string,string> bulidMap(ifstream &map_file) {map<string, string> trans_map; // 保存轉換規則string key; // 要轉換的單詞string value; // 替換后的內容// 讀取第一個單詞存入 key 中,行中剩余內容存入 valuewhile (map_file >> key && getline(map_file, value)){if (value.size() > 1) // 檢查是否有轉換規則{trans_map[key] = value.substr(1);// 跳過前導空格}else{throw runtime_error("no rule for " + key);}}return trans_map; }map_file 中的每一行對應一條規則。每條規則由一個單詞和一個短語組成,短語可能包含多個單詞。我們用 >> 讀取要轉換的單詞,存入 key 中,并用 getline 讀取這一行中的剩余內容存入 value 。由于 getline 不會跳過前導空格(3.2.2節),需要我們來跳過單詞和它的轉換內容之間的空格。在保存轉換規則之前,檢查是否獲得了一個以上的字符。如果是,調用 substr(9.5.1節)來跳過分隔單詞及其轉換短語之間的前導空格,并將得到的子字符存入 trans_map 。
注意,我們使用下標運算符來添加關鍵字-值對。我們隱含地忽略了一個單詞在轉換文件中出現多次的情況。如果真的有單詞出現多次,循環會將最后一個對應短語存入 trans_map 。當 while 循環結束后,trans_map 中將保存著用來轉換輸入文本的規則。
生成轉換文本
函數 transform 進行實際的轉換工作。其參數是需要轉換的 string 的引用和轉換規則 map。如果給定 string 在 map 中,transform 返回相應的短語。否則,transform 直接返回 string:
const string& transform(const string &s, const map<string, string> &m) {// 實際的轉換工作;此部分是程序的核心auto map_it = m.find(s);// 如果單詞在轉換規則 map 中if (map_it != m.cend()){return map_it->second; // 使用替換短語}else{return s; // 否則返回原 string} }函數調用 find 來確定給定 string 是否在 map 中。如果存在,則 find 返回一個指向對應元素的迭代器。否則,find 返回尾后迭代器。如果元素存在,我們解引用迭代器,獲得一個保存關鍵字和值的 pair (11.3節),然后返回成員 second,即用來替代 s 的內容。
11.3.6 節練習
練習 11.33:實現你自己版本的單詞轉換程序。
map<string,string> bulidMap(ifstream &map_file) {map<string, string> trans_map; // 保存轉換規則string key; // 要轉換的單詞string value; // 替換后的內容// 讀取第一個單詞存入 key 中,行中剩余內容存入 valuewhile (map_file >> key && getline(map_file, value)){if (value.size() > 1) // 檢查是否有轉換規則{trans_map[key] = value.substr(1);// 跳過前導空格}else{throw runtime_error("no rule for " + key);}}return trans_map; } const string& transform(const string &s, const map<string, string> &m) {// 實際的轉換工作;此部分是程序的核心auto map_it = m.find(s);// 如果單詞在轉換規則 map 中if (map_it != m.cend()){return map_it->second; // 使用替換短語}else{return s; // 否則返回原 string} } void word_transform(ifstream &map_file, ifstream &input) {auto trans_map = bulidMap(map_file); // 保存轉換規則string text; // 保存輸入中的每一行while (getline(input, text)) // 讀取一行輸入{istringstream stream(text); // 讀取每個單詞string word;bool firstword = true; // 控制是否打印空格while (stream >> word){if (firstword)firstword = false;elsecout << " "; // 在單詞間打印出一個空格// transform 返回它的第一個參數或其轉換之后的形式cout << transform(word, trans_map); //打印輸出}cout << endl; // 完成一行的轉換} }練習 11.34:如果你將 transform 函數中的 find 替換為下標運算符,會發生什么情況?
find 僅會查找給定關鍵字在容器中是否出現,若容器中不存在給定關鍵字,它返回尾后迭代器。當關鍵字存在時,下標運算符的行為與 find 類似,但當關鍵字不存在時,它會構造一個pair (進行值初始化),將其插入到容器中。對于單詞轉換程序,這會將不存在的內容插入到輸出文本中,這顯然不是我們所期望的。
練習 11.35:在 buildMap 中,如果進行如下改寫,會有什么效果?
trans_map[key] = value.substr(1); 改為 trans_map({key, value.substr(1)})當 map 中沒有給定關鍵字時,insert 操作與下標操作+賦值操作的效果類似,都是將關鍵字和值的 pair 添加到 map 中。
但當 map 中已有給定關鍵字,也就是新的轉換規則與上一條已有規則要轉換同一個單詞時,兩者的行為是不同的。下標操作會獲得具有該關鍵字的元素(也就是已有規則)的值,并將新讀入的值賦予它,也就是用心讀入的規則覆蓋了容器中的已有規則。但 insert 操作遇到關鍵字已存在的情況,則不會改變容器內容,而是返回一個值指出插入失敗。因此,當規則文件中存在多條規則轉換相同單詞時,下標+賦值的版本最終會用最后一條規則進行文本轉換,而 insert 版本則會用第一條規則進行文本轉換。
練習 11.36:我們的程序并沒有檢查輸入文件的合法性。特別是,它假設轉換規則文件中的規則都是有意義的。如果文件中的某一行包含一個關鍵字、一個空格,然后就結束了,會發生什么?預測程序的行為并進行驗證,再與你的程序進行比較。
11.4 無序容器
tring word;
bool firstword = true; // 控制是否打印空格
while (stream >> word)
{
if (firstword)
firstword = false;
else
cout << " "; // 在單詞間打印出一個空格
// transform 返回它的第一個參數或其轉換之后的形式
cout << transform(word, trans_map); //打印輸出
}
cout << endl; // 完成一行的轉換
}
}
trans_map[key] = value.substr(1);
改為 trans_map({key, value.substr(1)})
總結
以上是生活随笔為你收集整理的第11 章 关联容器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: aimesh node重启_ASSIMP
- 下一篇: AppCon 混合式开发