《C++ Primer 5th》笔记(11 / 19):关联容器
文章目錄
- 使用關(guān)聯(lián)容器
- 使用map
- 使用set
- 關(guān)聯(lián)容器概述
- 定義關(guān)聯(lián)容器
- 初始化multimap 或 multiset
- 關(guān)鍵字類型的要求
- 有序容器的關(guān)鍵字類型
- 使用關(guān)鍵字類型的比較函數(shù)
- pair類型
- 創(chuàng)建pair對象的函數(shù)
- 關(guān)聯(lián)容器操作
- 關(guān)聯(lián)容器迭代器
- set的迭代器是const的
- 遍歷關(guān)聯(lián)容器
- 關(guān)聯(lián)容器和算法
- 添加元素
- 向map添加元素
- 檢測insert的返回值
- 展開遞增語句
- 向multiset或multimap添加元素
- 刪除元素
- map的下標操作
- 使用下標操作的返回值
- 訪問元素
- 對map使用find代替下標操作
- 在multimap或multiset中查找元素
- 在multimap或multiset中查找元素,另一種不同的,面向迭代器的解決方法
- 在multimap或multiset中查找元素的另一種解法:equal_range函數(shù)
- 例程:一個單詞轉(zhuǎn)換的map
- 單詞轉(zhuǎn)換程序word_transform
- 建立轉(zhuǎn)換映射buildMap
- 生成轉(zhuǎn)換文本transform
- 無序容器
- 使用無序容器
- 管理桶
- 無序容器對關(guān)鍵字類型的需求
關(guān)聯(lián)容器支持高效的關(guān)鍵字查找和訪問。兩個主要的關(guān)聯(lián)容器(associative-container)類型是map和set。
- map中的元素是一些關(guān)鍵字-值(key-value)對:關(guān)鍵字起到索引的作用,值則表示與索引相關(guān)聯(lián)的數(shù)據(jù)。字典則是一個很好的使用map 的例子:可以將單詞作為關(guān)鍵字,將單詞釋義作為值。
- set中每個元素只包含一個關(guān)鍵字;set支持高效的關(guān)鍵字查詢操作——檢查一個給定關(guān)鍵字是否在set中。例如,在某些文本處理過程中,可以用一個set來保存想要忽略的單詞。
標準庫提供8個關(guān)聯(lián)容器,如下表所示。這8個容器間的不同體現(xiàn)在三個維度上:每個容器
| 按關(guān)鍵字有序保存元素 | |
| map | 關(guān)聯(lián)數(shù)組;保存關(guān)鍵字-值對 |
| set | 關(guān)鍵字即值,即只保存關(guān)鍵字的容器 |
| multimap | 關(guān)鍵字可重復(fù)出現(xiàn)的map |
| multiset | 關(guān)鍵字可重復(fù)出現(xiàn)的set |
| 無序集合 | |
| unordered_map | 用哈希函數(shù)組織的map |
| unordered_set | 用哈希函數(shù)組織的set |
| unordered_multimap | 哈希組織的map;關(guān)鍵字可以重復(fù)出現(xiàn) |
| unordered_multiset | 哈希組織的set;關(guān)鍵字可以重復(fù)出現(xiàn) |
(MyNote:與Java的TreeMap和HashMap比較學習。)
允許重復(fù)關(guān)鍵字的容器的名字中都包含單詞multi;不保持關(guān)鍵字按順序存儲的容器的名字都以單詞unordered開頭。因此一個unordered_multi_set是一個允許重復(fù)關(guān)鍵字,元素無序保存的集合,而一個set則是一個要求不重復(fù)關(guān)鍵字,有序存儲的集合。無序容器使用哈希函數(shù)來組織元素。
- 類型map和multimap定義在頭文件map中;
- set和multiset定義在頭文件set中;
- 無序容器則定義在頭文件unordered_map和unordered_set中。
使用關(guān)聯(lián)容器
map是關(guān)鍵字-值對的集合。例如,可以將一個人的名字作為關(guān)鍵字,將其電話號碼作為值。我們稱這樣的數(shù)據(jù)結(jié)構(gòu)為“將名字映射到電話號碼”。map類型通常被稱為關(guān)聯(lián)數(shù)組(associative array)。關(guān)聯(lián)數(shù)組與“正常”數(shù)組類似,不同之處在于其下標不必是整數(shù)。我們通過一個關(guān)鍵字而不是位置來查找值。給定一個名字到電話號碼的map,我們可以使用一個人的名字作為下標來獲取此人的電話號碼。
與之相對,set就是關(guān)鍵字的簡單集合。當只是想知道一個值是否存在時,set是最有用的。例如,一個企業(yè)可以定義一個名為bad_checks的set來保存那些曾經(jīng)開過空頭支票的人的名字。在接受一張支票之前,可以查詢bad_checks來檢查顧客的名字是否在其中。
使用map
一個經(jīng)典的使用關(guān)聯(lián)數(shù)組的例子是單詞計數(shù)程序:
//統(tǒng)計每個單詞在輸入中出現(xiàn)的次數(shù) map<string, size_t> word_count; // string到size_t的空mapstring word; while (cin >> word)++word_count[word]; //提取word的計數(shù)器并將其加1 for (const auto &w : word_count)//對map中的每個元素//打印結(jié)果cout<< w.first << " occurs " << w.second<< ((w.second > 1) ? " times" : " time") << endl;此程序讀取輸入,報告每個單詞出現(xiàn)多少次。
類似順序容器,關(guān)聯(lián)容器也是模板。為了定義一個map,我們必須指定關(guān)鍵字和值的類型。在此程序中,map保存的每個元素中,關(guān)鍵字是string類型,值是size_t類型。當對word_count進行下標操作時,我們使用一個string作為下標,獲得與此string相關(guān)聯(lián)的size_t類型的計數(shù)器。
C++語言既有類模板(class template),也有函數(shù)模板,其中 vector是一個類模板。只有對C++有了相當深入的理解才能寫出模板,事實上,我們直到第16章才會學習如何自定義模板。幸運的是,即使還不會創(chuàng)建模板,我們也可以先試著用用它。
模板本身不是類或函數(shù),相反可以將模板看作為編譯器生成類或函數(shù)編寫的一份說明。編譯器根據(jù)模板創(chuàng)建類或函數(shù)的過程稱為實例化(instantiation),當使用模板時,需要指出編譯器應(yīng)把類或函數(shù)實例化成何種類型。
對于類模板來說,我們通過提供一些額外信息來指定模板到底實例化成什么樣的類,需要提供哪些信息由模板決定。提供信息的方式總是這樣:即在模板名字后面跟一對尖括號,在括號內(nèi)放上信息。
第6章
while循環(huán)每次從標準輸入讀取一個單詞。它使用每個單詞對word_count進行下標操作。如果word還未在map中,下標運算符會創(chuàng)建一個新元素,其關(guān)鍵字為word,值為0。不管元素是否是新創(chuàng)建的,我們將其值加1。
一旦讀取完所有輸入,范圍for語句就會遍歷map,打印每個單詞和對應(yīng)的計數(shù)器。
當從map中提取一個元素時,會得到一個pair類型的對象。簡單來說,pair是一個模板類型,保存兩個名為first和second的(公有)數(shù)據(jù)成員。map所使用的pair用first成員保存關(guān)鍵字,用second成員保存對應(yīng)的值。因此,輸出語句的效果是打印每個單詞及其關(guān)聯(lián)的計數(shù)器。
使用set
上一個示例程序的一個合理擴展是:忽略常見單詞,如"the"、“and”、"or"等。我們可以使用set保存想忽略的單詞,只對不在集合中的單詞統(tǒng)計出現(xiàn)次數(shù):
//統(tǒng)計輸入中每個單詞出現(xiàn)的次數(shù) map<string,size_t> word_count; //string 到 size_t的空map //想忽略的單詞 set<string> exclude = { "The", "But", "And", "Or", "An", "A","the", "but", "and", "or", "an", "a"}; string word; while (cin >> word)//只統(tǒng)計不在exclude中的單詞if (exclude.find(word) == exclude.end())++word_count[word] ;//獲取并遞增word的計數(shù)器與其他容器類似,set也是模板。為了定義一個set,必須指定其元素類型,本例中是string。與順序容器類似,可以對一個關(guān)聯(lián)容器的元素進行列表初始化。集合exclude中保存了12個我們想忽略的單詞。
此程序與前一個程序的重要不同是,在統(tǒng)計每個單詞出現(xiàn)次數(shù)之前,我們檢查單詞是否在忽略集合中,這是在if語句中完成的:
//只統(tǒng)計不在exclude中的單詞 if(exclude.find(word) == exclude.end())find調(diào)用返回一個迭代器。如果給定關(guān)鍵字在set中,迭代器指向該關(guān)鍵字。否則,find返回尾后迭代器。在此程序中,僅當word不在exclude中時我們才更新word的計數(shù)器。
關(guān)聯(lián)容器概述
關(guān)聯(lián)容器(有序的和無序的)都支持第9章中介紹的普通容器操作。
關(guān)聯(lián)容器不支持順序容器的位置相關(guān)的操作,例如 push_front或push_back。原因是關(guān)聯(lián)容器中元素是根據(jù)關(guān)鍵字存儲的,這些操作對關(guān)聯(lián)容器沒有意義。而且,關(guān)聯(lián)容器也不支持構(gòu)造函數(shù)或插入操作這些接受一個元素值和一個數(shù)量值的操作。
除了與順序容器相同的操作之外,關(guān)聯(lián)容器還支持一些順序容器不支持的操作和類型別名。此外,無序容器還提供一些用來調(diào)整哈希性能的操作,隨后介紹。
關(guān)聯(lián)容器的迭代器都是雙向的。
定義關(guān)聯(lián)容器
如前所示,
- 當定義一個map時,必須既指明關(guān)鍵字類型又指明值類型;
- 而定義一個set 時,只需指明關(guān)鍵字類型,因為set中沒有值。
每個關(guān)聯(lián)容器都定義了一個默認構(gòu)造函數(shù),它創(chuàng)建一個指定類型的空容器。我們也可以將關(guān)聯(lián)容器初始化為另一個同類型容器的拷貝,或是從一個值范圍來初始化關(guān)聯(lián)容器,只要這些值可以轉(zhuǎn)化為容器所需類型就可以。在新標準下,我們也可以對關(guān)聯(lián)容器進行值初始化:
map<string, size_t> word_count; //空容器//列表初始化 set<string> exclude = { "the", "but", "and", "or", "an", "a","The", "But", "And", "or", "An", "A"};//三個元素; authors將姓映射為名 map<string, string> authors = {{ "Joyce","James"},{"Austen", "Jane"},{"Dickens", "Charles"}};與以往一樣,初始化器必須能轉(zhuǎn)換為容器中元素的類型。對于set,元素類型就是關(guān)鍵字類型。
當初始化一個map時,必須提供關(guān)鍵字類型和值類型。我們將每個關(guān)鍵字-值對包圍在花括號中:
{key, value}來指出它們一起構(gòu)成了map中的一個元素。在每個花括號中,關(guān)鍵字是第一個元素,值是第二個。因此,authors 將姓映射到名,初始化后它包含三個元素。
初始化multimap 或 multiset
一個map或set 中的關(guān)鍵字必須是唯一的,即,對于一個給定的關(guān)鍵字,只能有一個元素的關(guān)鍵字等于它。容器multimap和multiset沒有此限制,它們都允許多個元素具有相同的關(guān)鍵字。例如,在我們用來統(tǒng)計單詞數(shù)量的map中,每個單詞只能有一個元素。另一方面,在一個詞典中,一個特定單詞則可具有多個與之關(guān)聯(lián)的詞義。
下面的例子展示了具有唯一關(guān)鍵字的容器與允許重復(fù)關(guān)鍵字的容器之間的區(qū)別。首先,我們將創(chuàng)建一個名為ivec的保存int 的vector,它包含20個元素:0到9每個整數(shù)有兩個拷貝。我們將使用此vector初始化一個set和一個multiset:
//定義一個有20個元素的vector,保存0到9每個整數(shù)的兩個拷貝 vector<int> ivec; for (vector<int>::size_type i = 0; i != 10 ; ++i){ivec.push_back(i);ivec.push_back(i); //每個數(shù)重復(fù)保存一次 }// iset包含來自ivec的不重復(fù)的元素; miset包含所有20個元素 set<int> iset (ivec.cbegin(), ivec.cend()) ; multiset<int> miset (ivec.cbegin(), ivec.cend());cout << ivec.size() << endl;//打印出20 cout << iset.size() << endl;//打印出10 cout << miset.size() << endl;//打印出20即使我們用整個ivec容器來初始化iset,它也只含有10個元素:對應(yīng)ivec中每個不同的元素。另一方面,miset有20個元素,與ivec 中的元素數(shù)量一樣多。
關(guān)鍵字類型的要求
關(guān)聯(lián)容器對其關(guān)鍵字類型有一些限制。對于無序容器中關(guān)鍵字的要求,隨后介紹。對于有序容器——map、multimap、set 以及multiset,關(guān)鍵字類型必須定義元素比較的方法。
默認情況下,標準庫使用關(guān)鍵字類型的<運算符來比較兩個關(guān)鍵字。在集合類型中,關(guān)鍵字類型就是元素類型;在映射類型中,關(guān)鍵字類型是元素的第一部分的類型。因此,上例中word_count 的關(guān)鍵字類型是string。類似的,exclude 的關(guān)鍵字類型也是string。
傳遞給排序算法的可調(diào)用對象必須滿足與關(guān)聯(lián)容器中關(guān)鍵字一樣的類型要求。
有序容器的關(guān)鍵字類型
可以向一個算法提供我們自己定義的比較操作,與之類似,也可以提供自己定義的操作來代替關(guān)鍵字上的<運算符。所提供的操作必須在關(guān)鍵字類型上定義一個嚴格弱序(strict weak ordering)。可以將嚴格弱序看作“小于等于”,雖然實際定義的操作可能是一個復(fù)雜的函數(shù)。無論我們怎樣定義比較函數(shù),它必須具備如下基本性質(zhì):
- 兩個關(guān)鍵字不能同時“小于等于”對方;如果k1“小于等于”k2,那么k2絕不能“小于等于”k1。
- 如果k1“小于等于”k2,且 k2“小于等于”k3,那么k1必須“小于等于”k3。(傳遞性)
- 如果存在兩個關(guān)鍵字,任何一個都不“小于等于”另一個,那么我們稱這兩個關(guān)鍵字是“等價”的。如果k1“等價于”k2,且k2“等價于”k3,那么k1必須“等價于”k3。(傳遞性)
如果兩個關(guān)鍵字是等價的(即,任何一個都不“小于等于”另一個),那么容器將它們視作相等來處理。當用作map 的關(guān)鍵字時,只能有一個元素與這兩個關(guān)鍵字關(guān)聯(lián),我們可以用兩者中任意一個來訪問對應(yīng)的值。
Note:在實際編程中,重要的是,如果一個類型定義了“行為正常”的 < 運算符,則它可以用作關(guān)鍵字類型。
使用關(guān)鍵字類型的比較函數(shù)
用來組織一個容器中元素的操作的類型也是該容器類型的一部分。**為了指定使用自定義的操作,必須在定義關(guān)聯(lián)容器類型時提供此操作的類型。**如前所述,用尖括號指出要定義哪種類型的容器,自定義的操作類型必須在尖括號中緊跟著元素類型給出。
在尖括號中出現(xiàn)的每個類型,就僅僅是一個類型而已。當我們創(chuàng)建一個容器(對象)時,才會以構(gòu)造函數(shù)參數(shù)的形式提供真正的比較操作(其類型必須與在尖括號中指定的類型相吻合)。
例如,我們不能直接定義一個Sales_data的multiset,因為Sales_data沒有<運算符。但是,可以定義一個名叫compareIsbn函數(shù)來定義一個multiset。此函數(shù)在Sales_data對象的 ISBN成員上定義了一個嚴格弱序。函數(shù)compareIsbn應(yīng)該像下面這樣定義:
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs){return lhs.isbn() < rhs.isbn(); }為了使用自己定義的操作,在定義multiset時我們必須提供兩個類型:關(guān)鍵字類型Sales_data,以及比較操作類型——應(yīng)該是一種函數(shù)指針類型,可以指向compareIsbn。當定義此容器類型的對象時,需要提供想要使用的操作的指針。
在本例中,我們提供一個指向compareIsbn的指針:
// bookstore中多條記錄可以有相同的ISBN // bookstore中的元素以 ISBN的順序進行排列 multiset<Sales_data, decltype(compareIsbn)*>bookstore(compareIsbn);此處,我們使用decltype來指出自定義操作的類型。記住,當用decltype來獲得一個函數(shù)指針類型時,必須加上一個*來指出我們要使用一個給定函數(shù)類型的指針。用compareIsbn來初始化bookstore對象,這表示當我們向bookstore添加元素時,通過調(diào)用compareIsbn來為這些元素排序。即,bookstore中的元素將按它們的ISBN成員的值排序。
可以用compareIsbn代替&compareIsbn作為構(gòu)造函數(shù)的參數(shù),因為當我們使用一個函數(shù)的名字時,在需要的情況下它會自動轉(zhuǎn)化為一個指針。當然,使用&compareIsbn的效果也是一樣的。
pair類型
在介紹關(guān)聯(lián)容器操作之前,我們需要了解名為pair的標準庫類型,它定義在頭文件utility 中。
一個pair保存兩個數(shù)據(jù)成員。類似容器,pair是一個用來生成特定類型的模板。當創(chuàng)建一個pair時,我們必須提供兩個類型名,pair 的數(shù)據(jù)成員將具有對應(yīng)的類型。兩個類型不要求一樣:
pair<string, string> anon; //保存兩個string pair<string, size_t> word_count; //保存一個string和一個size_t pair<string, vector<int>> line; //保存string和vector<int>pair的默認構(gòu)造函數(shù)對數(shù)據(jù)成員進行值初始化。因此,
- anon是一個包含兩個空string 的 pair,
- word_count 中的size_t成員值為0,而string成員被初始化為空。
- line保存一個空 string和一個空vector。
我們也可以為每個成員提供初始化器:
pair<string, string> author{"James", "Joyce"};這條語句創(chuàng)建一個名為author的 pair,兩個成員被初始化為"James"和"Joyce"。
與其他標準庫類型不同,pair的數(shù)據(jù)成員是public的。兩個成員分別命名為first和 second。我們用普通的成員訪問符號來訪問它們,例如,上面的單詞計數(shù)程序的輸出語句中我們就是這么做的:
//打印結(jié)果 cout << w.first << " occurs " << w.second<< ((w.second > 1) ? " times" : " time") <<endl;此處,w是指向map中某個元素的引用。map 的元素是pair。在這條語句中,我們首先打印關(guān)鍵字——元素的first成員,接著打印計數(shù)器——second成員。標準庫只定義了有限的幾個pair操作,下表列出了這些操作。
| pair<T1, T2> p; | p是一個pair,兩個類型分別為T1和T2的成員都進行了值初始化 |
| pair<T1, T2> p(v1, v2) | p是一個成員類型為T1和T2的pair; first和second成員分別用v1和v2進行初始化 |
| pair<Tl,T2> p = {v1,v2}; | 等價于p(v1, v2) |
| make_pair(v1,v2) | 返回一個用v1和v2初始化的pair。pair的類型從v1和v2的類型推斷出來 |
| p.first | 返回p的名為first的(公有)數(shù)據(jù)成員 |
| p.second | 返回p的名為second的(公有)數(shù)據(jù)成員 |
| pl relop p2 | 關(guān)系運算符(<、>、<=、>=)按字典序定義:例如,當p1.first < p2.first或! (p2.first < p1.first) && pl.second < p2.second 成立時,p1 < p2為 true。關(guān)系運算利用元素的 < 運算符來實現(xiàn) |
| pl == p2 | 當first和second成員分別相等時,兩個pair相等。相等性判斷利用元素的==運算符實現(xiàn) |
| p1 != p2 | 共享上一條說明 |
創(chuàng)建pair對象的函數(shù)
想象有一個函數(shù)需要返回一個pair。在新標準下,我們可以對返回值進行列表初始化
pair<string, int> process (vector<string> &v){//處理vif(!v.empty())return {v.back(), v.back().size()};//列表初始化elsereturn pair<string, int>();//隱式構(gòu)造返回值 }若v不為空,我們返回一個由v中最后一個string及其大小組成的pair。否則,隱式構(gòu)造一個空pair,并返回它。
在較早的C++版本中,不允許用花括號包圍的初始化器來返回pair這種類型的對象,必須顯式構(gòu)造返回值:
if (!v.empty())return pair<string,int>(v.back(), v.back().size());我們還可以用make_pair來生成pair對象,pair的兩個類型來自于make_pair的參數(shù):
if (!v.empty())return make_pair(v.back(), v.back().size());關(guān)聯(lián)容器操作
除了第9章的容器庫概覽中列出的類型,關(guān)聯(lián)容器還定義了下表中列出的類型。這些類型表示容器關(guān)鍵字和值的類型。
| key_type | 此容器類型的關(guān)鍵字類型 |
| mapped_type | 每個關(guān)鍵字關(guān)聯(lián)的類型;只適用于map |
| value_type | 對于set,與key_type相同 對于map,為pair<const key_type, mapped_type> |
- 對于set類型,key_type和value_type是一樣的; set中保存的值就是關(guān)鍵字。
- 在一個map中,元素是關(guān)鍵字-值對。即,每個元素是一個pair對象,包含一個關(guān)鍵字和一個關(guān)聯(lián)的值。
由于我們不能改變一個元素的關(guān)鍵字,因此這些pair的關(guān)鍵字部分是const的:
set<string>::value_type vl; //v1是一個string set<string>::key_type v2; //v2是一個string map<string, int>::value_type v3; //v3是一個pair<const string, int> map<string, int>::key_type v4; //v4是一個string map<string, int>::mapped_type v5; //v5是一個int與順序容器一樣,我們使用作用域運算符來提取一個類型的成員——例如,map<string,int>::key_type。
只有map類型(unordered_map、unordered_multimap、multimap和map)才定義了mapped_type。
關(guān)聯(lián)容器迭代器
當解引用一個關(guān)聯(lián)容器迭代器時,我們會得到一個類型為容器的value_type的值的引用。對map而言,value_type是一個pair類型,其first成員保存const的關(guān)鍵字,second成員保存值:
//獲得指向word_count中一個元素的迭代器 auto map_it = word_count.begin(); //*map_it是指向一個pair<const string,size_t>對象的引用cout << map_it->first; //打印此元素的關(guān)鍵字 cout << " " <<map_it->second;//打印此元素的值map_it->first = "new key"; //錯誤:關(guān)鍵字是const的 ++map_it->second; //正確:我們可以通過迭代器改變元素Note:必須記住,一個map的value_type是一個pair,我們可以改變pair的值,但不能改變關(guān)鍵字成員的值。
set的迭代器是const的
雖然set類型同時定義了iterator和const_iterator類型,但兩種類型都只允許只讀訪問set中的元素。與不能改變一個map元素的關(guān)鍵字一樣,一個set中的關(guān)鍵字也是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中的關(guān)鍵字是只讀的cout<< *set_it <<endl; //正確:可以讀關(guān)鍵字 }遍歷關(guān)聯(lián)容器
map和set類型都支持的begin和end操作。與往常一樣,我們可以用這些函數(shù)獲取迭代器,然后用迭代器來遍歷容器。例如,我們可以編寫一個循環(huán)來打印本文初的中單詞計數(shù)程序的結(jié)果,如下所示:
//獲得一個指向首元素的迭代器 auto map_it = word_count.cbegin();//比較當前迭代器和尾后迭代器 while (map_it != word_count.cend()) {//解引用迭代器,打印關(guān)鍵字-值對cout << map_it->first << " occurs "<< map_it->second << " times" << endl;++map_it; //遞增迭代器,移動到下一個元素 }Note:本程序的輸出是按字典序排列的。當使用一個迭代器遍歷一個map、multimap、set或multiset時,迭代器按關(guān)鍵字升序遍歷元素。
關(guān)聯(lián)容器和算法
我們通常不對關(guān)聯(lián)容器使用泛型算法(參見第10章)。關(guān)鍵字是const這一特性意味著不能將關(guān)聯(lián)容器傳遞給修改或重排容器元素的算法,因為這類算法需要向元素寫入值,而set類型中的元素是const的,map中的元素是pair,其第一個成員是const的。
關(guān)聯(lián)容器可用于只讀取元素的算法。但是,很多這類算法都要搜索序列。由于關(guān)聯(lián)容器中的元素不能通過它們的關(guān)鍵字進行(快速)查找,因此對其使用泛型搜索算法幾乎總是個壞主意。例如,我們將在第11章中看到,關(guān)聯(lián)容器定義了一個名為find的成員,它通過一個給定的關(guān)鍵字直接獲取元素。我們可以用泛型find算法來查找一個元素,但此算法會進行順序搜索。使用關(guān)聯(lián)容器定義的專用的find成員會比調(diào)用泛型find快得多。
在實際編程中,如果我們真要對一個關(guān)聯(lián)容器使用算法,
- 要么是將它當作一個源序列。例如,可以用泛型copy算法將元素從一個關(guān)聯(lián)容器拷貝到另一個序列。
- 要么當作一個目的位置。例如,可以調(diào)用inserter將一個插入器綁定(參見第10章)到一個關(guān)聯(lián)容器。通過使用inserter,我們可以將關(guān)聯(lián)容器當作一個目的位置來調(diào)用另一個算法。
添加元素
| c.insert(v) c.emplace(args) | v是value_type類型的對象;args用來構(gòu)造一個元素 對于map和set,只有當元素的關(guān)鍵字不在c中時才插入(或構(gòu)造)元素。 函數(shù)返回一個pair,包含一個迭代器,指向具有指定關(guān)鍵字的元素,以及一個指示插入是否成功的bool值。 對于multimap和multiset,總會插入(或構(gòu)造)給定元素,并返回一個指向新元素的迭代器 |
| c.insert(b, e) c.insert(il) | b和e是迭代器,表示一個c::value_type類型值的范圍; il是這種值的花括號列表。 函數(shù)返回void。 對于map和set,只插入關(guān)鍵字不在c中的元素。對于multimap和multiset,則會插入范圍中的每個元素 |
| c.insert(p, v) c.emplace(p, args) | 類似insert(v)(或emplace(args)),但將迭代器p作為一個提示,指出從哪里開始搜索新元素應(yīng)該存儲的位置。返回一個迭代器,指向具有給定關(guān)鍵字的元素 |
關(guān)聯(lián)容器的insert成員向容器中添加一個元素或一個元素范圍。由于 map和set(以及對應(yīng)的無序類型)包含不重復(fù)的關(guān)鍵字,因此插入一個已存在的元素對容器沒有任何影響:
vector<int> ivec = { 2,4,6,8,2,4,6,8 }; //ivec有8個元素 set<int> set2; //空集合 set2.insert(ivec.cbegin(), ivec.cend()); //set2有4個元素 set2.insert({1,3,5,7,1,3,5,7}); //set2又加入4個元素,現(xiàn)在已有8個元素insert有兩個版本,分別接受一對迭代器,或是一個初始化器列表,這兩個版本的行為類似對應(yīng)的構(gòu)造函數(shù)——對于一個給定的關(guān)鍵字,只有第一個帶此關(guān)鍵字的元素才被插入到容器中。
向map添加元素
對一個map進行 insert 操作時,必須記住元素類型是pair。通常,對于想要插入的數(shù)據(jù),并沒有一個現(xiàn)成的pair對象。可以在insert的參數(shù)列表中創(chuàng)建一個pair:
//向word_count插入word的4種方法 word_count.insert({word, 1}); word_count.insert(make_pair(word, 1)); word_count.insert(pair<string,size_t>(word, 1)); word_count.insert(map<string,size_t>::value_type(word, 1));如我們所見,在新標準下,創(chuàng)建一個pair最簡單的方法是在參數(shù)列表中使用花括號初始化。也可以調(diào)用make_pair或顯式構(gòu)造pair。最后一個insert調(diào)用中的參數(shù):
map<string, size_t>::value_type(s, 1)構(gòu)造一個恰當?shù)膒air類型,并構(gòu)造該類型的一個新對象,插入到map中。
檢測insert的返回值
insert(或emplace)返回的值依賴于容器類型和參數(shù)。對于不包含重復(fù)關(guān)鍵字的容器,添加單一元素的insert和 emplace版本返回一個pair,告訴我們插入操作是否成功。
pair的first成員是一個迭代器,指向具有給定關(guān)鍵字的元素;second成員是一個bool值,指出元素是插入成功還是已經(jīng)存在于容器中。
- 如果關(guān)鍵字已在容器中,則insert什么事情也不做,且返回值中的bool部分為false。
- 如果關(guān)鍵字不存在,元素被插入容器中,且bool值為true。
作為一個例子,我們用insert重寫單詞計數(shù)程序:
// more verbose way to count number of times each word occurs in the input map<string, size_t> word_count; // empty map from string to size_t string word; while (cin >> word) {// inserts an element with key equal to word and value 1;// if word is already in word_count, insert does nothingauto ret = word_count.insert({word, 1});if (!ret.second) // word was already in word_count++ret.first->second; // increment the counter }展開遞增語句
在這個版本的單詞計數(shù)程序中,遞增計數(shù)器的語句很難理解。通過添加一些括號來反映出運算符的優(yōu)先級,會使表達式更容易理解一些:
++((ret.first)->second); //等價的表達式下面我們一步一步來解釋此表達式:
- ret保存insert返回的值,是一個pair。
- ret.first是pair的第一個成員,是一個map迭代器,指向具有給定關(guān)鍵字的元素。
- ret.first->解引用此迭代器,提取map中的元素,元素也是一個pair。
- ret.first->second map 中元素的值部分。
- ++ret.first->second遞增此值。
再回到原來完整的遞增語句,它提取匹配關(guān)鍵字word的元素的迭代器,并遞增與我們試圖插入的關(guān)鍵字相關(guān)聯(lián)的計數(shù)器。
如果讀者使用的是舊版本的編譯器,或者是在閱讀新標準推出之前編寫的代碼,ret的聲明和初始化可能復(fù)雜些:
pair<map<string, size_t>::iterator, bool> ret =word_count.insert(make_pair(word, 1));應(yīng)該容易看出這條語句定義了一個pair,其第二個類型為 bool類型。第一個類型理解起來有點兒困難,它是一個在map<string, size_t>類型上定義的iterator類型。
向multiset或multimap添加元素
我們的單詞計數(shù)程序依賴于這樣一個事實:一個給定的關(guān)鍵字只能出現(xiàn)一次。這樣,任意給定的單詞只有一個關(guān)聯(lián)的計數(shù)器。我們有時希望能添加具有相同關(guān)鍵字的多個元素。
例如,可能想建立作者到他所著書籍題目的映射。在此情況下,每個作者可能有多個條目,因此我們應(yīng)該使用multimap而不是map。由于一個multi容器中的關(guān)鍵字不必唯一,在這些類型上調(diào)用insert總會插入一個元素:
multimap<string, string> authors; //插入第一個元素,關(guān)鍵字為Barth,John authors.insert({"Barth,John", "Sot-weed Factor"});//正確:添加第二個元素,關(guān)鍵字也是Barth,John authors.insert({"Barth,John", "Lost in the Funhouse"));對允許重復(fù)關(guān)鍵字的容器,接受單個元素的insert操作返回一個指向新元素的迭代器。這里無須返回一個bool值,因為insert總是向這類容器中加入一個新元素。
刪除元素
關(guān)聯(lián)容器定義了三個版本的erase,如下表所示。與順序容器一樣,我們可以通過傳遞給erase一個迭代器或一個迭代器對來刪除一個元素或者一個元素范圍。這兩個版本的erase與對應(yīng)的順序容器的操作非常相似:指定的元素被刪除,函數(shù)返回void。
| c.erase(k) | 從c中刪除每個關(guān)鍵字為k的元素。返回一個size_type值,指出刪除的元素的數(shù)量 |
| c.erase§ | 從c中刪除迭代器p指定的元素。p必須指向c中一個真實元素,不能等于c.end()。返回一個指向p之后元素的迭代器,若p指向c中的尾元素,則返回c.end() |
| c.erase(b, e) | 刪除迭代器對b和e所表示的范圍中的元素。返回e |
關(guān)聯(lián)容器提供一個額外的erase操作,它接受一個key_type參數(shù)。此版本刪除所有匹配給定關(guān)鍵字的元素(如果存在的話),返回實際刪除的元素的數(shù)量。我們可以用此版本在打印結(jié)果之前從word_count中刪除一個特定的單詞:
//刪除一個關(guān)鍵字,返回刪除的元素數(shù)量 if (word_count.erase(removal_word))cout << "ok: " << removal_word << " removed\n"; else cout << "oops: " << removal_word << " not found!\n";對于保存不重復(fù)關(guān)鍵字的容器,erase的返回值總是0或1。若返回值為0,則表明想要刪除的元素并不在容器中。
對允許重復(fù)關(guān)鍵字的容器,刪除元素的數(shù)量可能大于1:
auto cnt = authors.erase("Barth, John");如果authors是我們在向multiset或multimap添加元素中創(chuàng)建的multimap,則cnt 的值為2。
map的下標操作
map和 unordered_map容器提供了下標運算符和一個對應(yīng)的at函數(shù),如下表所示。
| c[k] | 返回關(guān)鍵字為k的元素;如果k不在c中,添加一個關(guān)鍵字為k的元素,對其進行值初始化 |
| c.at(k) | 訪問關(guān)鍵字為k的元素,帶參數(shù)檢查;若k不在c中,拋出一個out_of_range異常 |
set類型不支持下標,因為set中沒有與關(guān)鍵字相關(guān)聯(lián)的“值”。元素本身就是關(guān)鍵字,因此“獲取與一個關(guān)鍵字相關(guān)聯(lián)的值”的操作就沒有意義了。
我們不能對一個multimap或一個unordered_multimap進行下標操作,因為這些容器中可能有多個值與一個關(guān)鍵字相關(guān)聯(lián)。
類似我們用過的其他下標運算符,map下標運算符接受一個索引(即,一個關(guān)鍵字),獲取與此關(guān)鍵字相關(guān)聯(lián)的值。但是,與其他下標運算符不同的是,如果關(guān)鍵字并不在map中,會為它創(chuàng)建一個元素并插入到map中,關(guān)聯(lián)值將進行值初始化。
例如,如果我們編寫如下代碼
map<string, size_t> word_count; //empty map //插入一個關(guān)鍵字為Anna的元素,關(guān)聯(lián)值進行值初始化;然后將1賦予它 word_count["Anna"] = 1;將會執(zhí)行如下操作:
-
在word_count中搜索關(guān)鍵字為Anna的元素,未找到。
-
將一個新的關(guān)鍵字-值對插入到word_count中。關(guān)鍵字是一個const string,保存Anna。值進行值初始化,在本例中意味著值為0。
-
提取出新插入的元素,并將值1賦予它。
由于下標運算符可能插入一個新元素,我們只可以對非const的map使用下標操作。
Note:對一個map使用下標操作,其行為與數(shù)組或vector上的下標操作很不相同:使用一個不在容器中的關(guān)鍵字作為下標,會添加一個具有此關(guān)鍵字的元素到map中。
使用下標操作的返回值
map的下標運算符與我們用過的其他下標運算符的另一個不同之處是其返回類型。
-
通常情況下,解引用一個迭代器所返回的類型與下標運算符返回的類型是一樣的。
-
但對map則不然,當對一個map進行下標操作時,會獲得一個mapped_type對象;但當解引用一個map迭代器時,會得到一個value_type對象。
與其他下標運算符相同的是,map 的下標運算符返回一個左值。由于返回的是一個左值,所以我們既可以讀也可以寫元素:
cout << word_count["Anna"]; //用Anna 作為下標提取元素;會打印出1 ++word_count["Anna"]; //提取元素,將其增1 cout << word_count["Anna"];//提取元素并打印它;會打印出2Note:與vector與string不同,map 的下標運算符返回的類型 與 解引用map迭代器得到的類型不同。
如果關(guān)鍵字還未在map 中,下標運算符會添加一個新元素,這一特性允許我們編寫出異常簡潔的程序。另一方面,有時只是想知道一個元素是否已在 map 中,但在不存在時并不想添加元素。在這種情況下,就不能使用下標運算符。
訪問元素
關(guān)聯(lián)容器提供多種查找一個指定元素的方法,如下表所示。應(yīng)該使用哪個操作依賴于我們要解決什么問題。
- 如果我們所關(guān)心的只不過是一個特定元素是否已在容器中,可能find是最佳選擇。
- 對于不允許重復(fù)關(guān)鍵字的容器,可能使用find還是count沒什么區(qū)別。但對于允許重復(fù)關(guān)鍵字的容器,count還會做更多的工作:
- 如果元素在容器中,count還會統(tǒng)計有多少個元素有相同的關(guān)鍵字。
- 如果不需要計數(shù),最好使用find:
對map使用find代替下標操作
對map和 unordered_map類型,下標運算符提供了最簡單的提取元素的方法。但是,如我們所見,使用下標操作有一個嚴重的副作用:如果關(guān)鍵字還未在map中,下標操作會插入一個具有給定關(guān)鍵字的元素。這種行為是否正確完全依賴于我們的預(yù)期是什么。例如,單詞計數(shù)程序依賴于這樣一個特性:使用一個不存在的關(guān)鍵字作為下標,會插入一個新元素,其關(guān)鍵字為給定關(guān)鍵字,其值為0。也就是說,下標操作的行為符合我們的預(yù)期。
但有時,我們只是想知道一個給定關(guān)鍵字是否在map 中,而不想改變map。這樣就不能使用下標運算符來檢查一個元素是否存在,因為如果關(guān)鍵字不存在的話,下標運算符會插入一個新元素。在這種情況下,應(yīng)該使用find:
if (word_count.find("foobar") == word_count.end())cout << "foobar is not in the map" << endl;在multimap或multiset中查找元素
在一個不允許重復(fù)關(guān)鍵字的關(guān)聯(lián)容器中查找一個元素是一件很簡單的事情——元素要么在容器中,要么不在。但對于允許重復(fù)關(guān)鍵字的容器來說,過程就更為復(fù)雜:在容器中可能有很多元素具有給定的關(guān)鍵字。如果一個multimap或multiset中有多個元素具有給定關(guān)鍵字,則這些元素在容器中會相鄰存儲。
例如,給定一個從作者到著作題目的映射,我們可能想打印一個特定作者的所有著作。可以用三種不同方法來解決這個問題。最直觀的方法是使用find和count:
string search_item("Alain de Botton");//要查找的作者 auto entries = authors.count(search_item);//元素的數(shù)量 auto iter = authors.find(search_item) ;//此作者的第一本書//用一個循環(huán)查找此作者的所有著作 while(entries) {cout << iter->second <<endl; //打印每個題目++iter;//前進到下一本書--entries;//記錄已經(jīng)打印了多少本書 }首先調(diào)用count確定此作者共有多少本著作,并調(diào)用find 獲得一個迭代器,指向第一個關(guān)鍵字為此作者的元素。for 循環(huán)的迭代次數(shù)依賴于count 的返回值。特別是,如果count返回0,則循環(huán)一次也不執(zhí)行。
Note:當我們遍歷一個multimap或multiset時,保證可以得到序列中所有具有給定關(guān)鍵字的元素。
在multimap或multiset中查找元素,另一種不同的,面向迭代器的解決方法
我們還可以用lower_bound和upper_bound來解決此問題。這兩個操作都接受一個關(guān)鍵字,返回一個迭代器。
如果關(guān)鍵字在容器中,
- lower_bound返回的迭代器將指向第一個具有給定關(guān)鍵字的元素,
- upper_bound返回的迭代器則指向最后一個匹配給定關(guān)鍵字的元素之后的位置。
如果元素不在multimap中,則lower_bound和upper_bound 會返回相等的迭代器——指向一個不影響排序的關(guān)鍵字插入位置。
因此,用相同的關(guān)鍵字調(diào)用lower_bound和upper_bound 會得到一個迭代器范圍,表示所有具有該關(guān)鍵字的元素的范圍。
(MyNote:與begin()、end()類似。)
當然,這兩個操作返回的迭代器可能是容器的尾后迭代器。如果我們查找的元素具有容器中最大的關(guān)鍵字,則此關(guān)鍵字的upper_bound返回尾后迭代器。如果關(guān)鍵字不存在,且大于容器中任何關(guān)鍵字,則lower_bound返回的也是尾后迭代器。
Note:lower_bound返回的迭代器可能指向一個具有給定關(guān)鍵字的元素,但也可能不指向。如果關(guān)鍵字不在容器中,則lower_bound會返回關(guān)鍵字的第一個安全插入點——不影響容器中元素順序的插入位置。
使用這兩個操作,我們可以重寫前面的程序:
// authors和search_item的定義,與前面的程序一樣 // beg和end表示對應(yīng)此作者的元素的范圍 for (auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item);beg != end; ++beg) cout << beg->second << endl; //打印每個題目此程序與使用count和 find的版本完成相同的工作,但更直接。
- 對lower_bound 的調(diào)用將beg定位到第一個與search_item 匹配的元素(如果存在的話)。如果容器中沒有這樣的元素,beg將指向第一個關(guān)鍵字大于search_item的元素,有可能是尾后迭代器。
- upper_bound調(diào)用將end指向最后一個匹配指定關(guān)鍵字的元素之后的元素。這兩個操作并不報告關(guān)鍵字是否存在,重要的是它們的返回值可作為一個迭代器范圍。
如果沒有元素與給定關(guān)鍵字匹配,則lower_bound和upper_bound會返回相等的迭代器——都指向給定關(guān)鍵字的插入點,能保持容器中元素順序的插入位置。
假定有多個元素與給定關(guān)鍵字匹配,beg將指向其中第一個元素。我們可以通過遞增beg來遍歷這些元素。end中的迭代器會指出何時完成遍歷——當beg 等于end時,就表明已經(jīng)遍歷了所有匹配給定關(guān)鍵字的元素了。
由于這兩個迭代器構(gòu)成一個范圍,我們可以用一個 for 循環(huán)來遍歷這個范圍。循環(huán)可能執(zhí)行零次,如果存在給定作者的話,就會執(zhí)行多次,打印出該作者的所有項。如果給定作者不存在,beg和end相等,循環(huán)就一次也不會執(zhí)行。否則,我們知道遞增beg最終會使它到達end,在此過程中我們就會打印出與此作者關(guān)聯(lián)的每條記錄。
Note:如果lower_bound和upper_bound返回相同的迭代器,則給定關(guān)鍵字不在容器中。
在multimap或multiset中查找元素的另一種解法:equal_range函數(shù)
解決此問題的最后一種方法是三種方法中最直接的:不必再調(diào)用upper_bound和lower_bound,直接調(diào)用equal_range即可。此函數(shù)接受一個關(guān)鍵字,返回一個迭代器pair。
-
若關(guān)鍵字存在,則第一個迭代器指向第一個與關(guān)鍵字匹配的元素,第二個迭代器指向最后一個匹配元素之后的位置。
-
若未找到匹配元素,則兩個迭代器都指向關(guān)鍵字可以插入的位置。
可以用equal_range來再次修改我們的程序:
// authors和search_item的定義,與前面的程序一樣 // pos 保存迭代器對,表示與關(guān)鍵字匹配的元素范圍 for (auto pos = authors.equal_range (search_item);pos.first != pos.second; ++pos.first)cout<< pos.first->second << endl;//打印每個題目此程序本質(zhì)上與前一個使用upper_bound和 lower_bound的程序是一樣的。
不同之處就是,沒有用局部變量beg和end 來保存元素范圍,而是使用了equal_range返回的pair。此pair的first成員保存的迭代器與lower_bound返回的迭代器是一樣的,second保存的迭代器與upper_bound 的返回值是一樣的。因此,在此程序中,pos.first等價于beg,pos.second等價于end。
例程:一個單詞轉(zhuǎn)換的map
我們將以一個程序結(jié)束本節(jié)的內(nèi)容,它將展示 map 的創(chuàng)建、搜索以及遍歷。
這個程序的功能是這樣的:給定一個string,將它轉(zhuǎn)換為另一個string。
程序的輸入是兩個文件:
- 第一個文件保存的是一些規(guī)則,用來轉(zhuǎn)換第二個文件中的文本。每條規(guī)則由兩部分組成:一個可能出現(xiàn)在輸入文件中的單詞和一個用來替換它的短語。表達的含義是,每當?shù)谝粋€單詞出現(xiàn)在輸入中時,我們就將它替換為對應(yīng)的短語。
- 第二個輸入文件包含要轉(zhuǎn)換的文本。
如果單詞轉(zhuǎn)換文件的內(nèi)容如下所示:
brb be right back k okay? y why r are u you pic picture thk thanks! 18r later我們希望轉(zhuǎn)換的文本為
where r u y dont u send me a pic k thk 18r則程序應(yīng)該生成這樣的輸出:
where are you why dont you send me a picture okay? thanks! later單詞轉(zhuǎn)換程序word_transform
我們的程序?qū)⑹褂萌齻€函數(shù)。
我們首先定義 word_transform函數(shù)。最重要的部分是調(diào)用buildMap和transform:
void word_transform(ifstream &map_file, ifstream &input) {auto trans_map = buildMap(map_file); // 1.store the transformations//2.string text; // hold each line from the inputwhile (getline(input, text)) { // 3.read a line of inputistringstream stream(text); // 4.read each wordstring word;bool firstword = true; // controls whether a space is printedwhile (stream >> word) {if (firstword)//5.firstword = false;elsecout << " "; // print a space between words// 6.transform returns its first argument or its transformationcout << transform(word, trans_map); // print the output}cout << endl; // done with this line of input} }建立轉(zhuǎn)換映射buildMap
函數(shù)buildMap讀入給定文件,建立起轉(zhuǎn)換映射。
map<string, string> buildMap(ifstream &map_file)//1. {map<string, string> trans_map; // holds the transformationsstring key; // a word to transformstring value; // phrase to use instead// read the first word into key and the rest of the line into valuewhile (map_file >> key && getline(map_file, value))//2. 3.if (value.size() > 1) // check that there is a transformation//4.trans_map[key] = value.substr(1); // skip leading space//5.elsethrow runtime_error("no rule for " + key);return trans_map;//6. }生成轉(zhuǎn)換文本transform
函數(shù)transform進行實際的轉(zhuǎn)換工作。其參數(shù)是需要轉(zhuǎn)換的string的引用和轉(zhuǎn)換規(guī)則map。如果給定string在map 中,transform返回相應(yīng)的短語。否則,transform直接返回原string:
const string & transform(const string &s, const map<string, string> &m) {// the actual map work; this part is the heart of the programauto map_it = m.find(s);//1. // if this word is in the transformation mapif (map_it != m.cend())//2. return map_it->second; // use the replacement wordelsereturn s; // otherwise return the original unchanged }無序容器
C++11新標準定義了4個無序關(guān)聯(lián)容器(unordered associative container)。這些容器不是使用比較運算符來組織元素,而是使用一個哈希函數(shù)(hash function)和關(guān)鍵字類型的==運算符。在關(guān)鍵字類型的元素沒有明顯的序關(guān)系的情況下,無序容器是非常有用的。在某些應(yīng)用中,維護元素的序代價非常高昂,此時無序容器也很有用。
雖然理論上哈希技術(shù)能獲得更好的平均性能,但在實際中想要達到很好的效果還需要進行一些性能測試和調(diào)優(yōu)工作。因此,使用無序容器通常更為簡單(通常也會有更好的性能)。
(MyNote: 與Java的HashMap類比學習。)
Tip:如果關(guān)鍵字類型固有就是無序的,或者性能測試發(fā)現(xiàn)問題可以用哈希技術(shù)解決,就可以使用無序容器。
使用無序容器
除了哈希管理操作之外,無序容器還提供了與有序容器相同的操作(find、insert等)。這意味著我們曾用于map和set的操作也能用于unordered_map 和unordered_set。類似的,無序容器也有允許重復(fù)關(guān)鍵字的版本。
因此,通常可以用一個無序容器替換對應(yīng)的有序容器,反之亦然。但是,由于元素未按順序存儲,一個使用無序容器的程序的輸出(通常)會與使用有序容器的版本不同。
例如,可以用unordered_map重寫最初的單詞計數(shù)程序:
// count occurrences, but the words won't be in alphabetical order unordered_map<string, size_t> word_count; string word;while (cin >> word)++word_count[word]; // fetch and increment the counter for word for (const auto &w : word_count) // for each element in the map// print the resultscout << w.first << " occurs " << w.second<< ((w.second > 1) ? " times" : " time") << endl;管理桶
無序容器在存儲上組織為一組桶,每個桶保存零個或多個元素。無序容器使用一個哈希函數(shù)將元素映射到桶。為了訪問一個元素,容器首先計算元素的哈希值,它指出應(yīng)該搜索哪個桶。容器將具有一個特定哈希值的所有元素都保存在相同的桶中。如果容器允許重復(fù)關(guān)鍵字,所有具有相同關(guān)鍵字的元素也都會在同一個桶中。因此,無序容器的性能依賴于哈希函數(shù)的質(zhì)量和桶的數(shù)量和大小。
對于相同的參數(shù),哈希函數(shù)必須總是產(chǎn)生相同的結(jié)果。理想情況下,哈希函數(shù)還能將每個特定的值映射到唯一的桶。但是,將不同關(guān)鍵字的元素映射到相同的桶也是允許的。當一個桶保存多個元素時,需要順序搜索這些元素來查找我們想要的那個。計算一個元素的哈希值和在桶中搜索通常都是很快的操作。但是,如果一個桶中保存了很多元素,那么查找一個特定元素就需要大量比較操作。
(MyNote:上述為無序容器的基本原理。)
(MyNote:哈希函數(shù)三個指標:高效,平均,唯一)
無序容器提供了一組管理桶的函數(shù),如下表所示。這些成員函數(shù)允許我們查詢?nèi)萜鞯臓顟B(tài)以及在必要時強制容器進行重組。
| 桶接口 | |
| c.bucket_count() | 正在使用的桶的數(shù)目 |
| c.max_bucket_count() | 容器能容納的最多的桶的數(shù)量 |
| c.bucket_size(n) | 第n個桶中有多少個元素 |
| c.bucket(k) | 關(guān)鍵字為k的元素在哪個桶中 |
| 桶迭代 | |
| local_iterator | 可以用來訪問桶中元素的迭代器類型 |
| const_local_iterator | 桶迭代器的const版本 |
| c.begin(n), c.end(n) | 桶n的首元素迭代器和尾后迭代器 |
| c.cbegin(n), c.cend(n) | 與前兩個函數(shù)類似,但返回const_local_iterator |
| 哈希策略 | |
| c.load_factor() | 每個桶的平均元素數(shù)量,返回float值 |
| c.max_load_factor() | c試圖維護的平均桶大小,返回float值。c會在需要時添加新的桶,以使得load_factor<=max_load_factor |
| c.rehash(n) | 重組存儲,使得bucket_count >= n 且 bucket_count > size / max_load_factor |
| c.reserve(n) | 重組存儲,使得c可以保存n個元素且不必rehash |
無序容器對關(guān)鍵字類型的需求
默認情況下,無序容器使用關(guān)鍵字類型的==運算符來比較元素,它們還使用一個hash<key_type>類型的對象來生成每個元素的哈希值。標準庫為內(nèi)置類型(包括指針)提供了hash模板。還為一些標準庫類型,包括string和我們將要在第12章介紹的智能指針類型定義了hash。因此,我們可以直接定義關(guān)鍵字是內(nèi)置類型(包括指針類型)、string還是智能指針類型的無序容器。
但是,我們不能直接定義關(guān)鍵字類型為自定義類類型的無序容器。與容器不同,不能直接使用哈希模板,而必須提供我們自己的hash模板版本。我們將在第16章中介紹如何做到這一點。
我們不使用默認的hash,而是使用另一種方法,類似于為有序容器重載關(guān)鍵字類型的默認比較操作使用關(guān)鍵字類型的比較函數(shù)。
為了能將Sale_data用作關(guān)鍵字,我們需要提供函數(shù)來替代==運算符和哈希值計算函數(shù)。我們從定義這些重載函數(shù)開始:
size_t hasher(const sales_data &sd) {return hash<string>(0)(sd.isbn()); } bool eqOp(const sales_data &lhs,const sales_data &rhs){return lhs.isbn() == rhs.isbn (); }我們的hasher函數(shù)使用一個標準庫hash類型對象來計算ISBN成員的哈希值,該hash類型建立在 string 類型之上。類似的,eqOp函數(shù)通過比較ISBN號來比較兩個Sales_data。
我們使用這些函數(shù)來定義一個unordered_multiset
using SD_multiset = unordered_multiset<sales_data,decltype(hasher)* , decltype(eqOp)*>; //參數(shù)是桶大小、哈希函數(shù)指針和相等性判斷運算符指針 SD_multiset bookstore(42, hasher, eqOp);為了簡化bookstore的定義,首先為unordered_multiset定義了一個類型別名,此集合的哈希和相等性判斷操作與hasher和eqOp函數(shù)有著相同的類型。通過使用這種類型,在定義bookstore時可以將我們希望它使用的函數(shù)的指針傳遞給它。
如果我們的類定義了==運算符,則可以只重載哈希函數(shù):
//使用FooHash生成哈希值;Foo必須有==運算符 unordered_set<Foo,decltype(FooHash)*> fooSet(10,FooHash);(MyNote:類似于Java類使用HashMap前,需要override hashCode()、equals()。)
總結(jié)
以上是生活随笔為你收集整理的《C++ Primer 5th》笔记(11 / 19):关联容器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《Java8实战》笔记(01):为什么要
- 下一篇: 剑指offer(刷题51-60)--c+