1. 程式人生 > >map和set的使用和原理

map和set的使用和原理

我們學習過順序容器如vecor,list等,它們中的元素是按照在容器中的位置來順序儲存和訪問的。而接下來要學習的關聯容器則有根本的不同,它們中的元素是按關鍵字來儲存和訪問的。
在《C++Primer》中列舉了標準庫中的8個關聯容器,如下:

這裡寫圖片描述

關聯容器支援高效的關鍵字查詢和訪問,我們在這裡介紹兩個主要的關聯容器set和map。

map

map裡面存的是一些key-value對,其中key起到索引的作用, 而value則表示於索引相關聯的資料。
比如字典就是一個很好使用map的例子,把單詞當作key,解釋當作value。其實map型別也常稱做關聯陣列,它和一般的陣列類似,可以認為它key就是陣列的下標(只不過不必是整數),value則是陣列存的值,還是上面的字典例子,比如有某個單詞為right,它的其中一個解釋為右邊的,我們就可以使用類似陣列的方式a[“right”]訪問到“右邊的”這個解釋。

set

set裡面每個元素只存有一個key,它支援高效的關鍵字查詢操作,比如檢查一個關鍵字是否在set中或者在
某些文字處理過程中可用set儲存想要忽略的單詞

在學習這兩種關聯容器之前,我們可以先看一兩個如何使用這類容器的例子。

假設一個字串陣列,裡面的每個字串都是水果,比如蘋果,梨子等,現在要統計每種水果出現的次數。

string fruit[] =
{
    "apple", "pear", "watermelon", "peach", "banana",
    "apple", "pear", "watermelon", "peach",
    "apple"
, "pear", "watermelon", "apple", "pear", "apple", }; size_t n = sizeof(fruit) / sizeof(fruit[0]); map<string, size_t> countFruit;//定義map用來存取每種水果和它的次數 for (size_t i = 0; i < n; ++i)//增加每種水果的次數 { ++countFruit[fruit[i]]; } for (const auto& element : countFruit)//列印每種水果以及它的次數 { cout
<< element.first << " : " << element.second << endl; }

對於上面的程式,首先定義了一個map來存取每種水果和它的次數,map也是模板,我們必須指定key和value,在這個程式中,key為string表示水果,value為size_t表示次數。

此時map裡面的每個元素都是一個pair型別的物件,簡單來說pair是個模版型別,儲存兩個public成員first和second分別對應key和value。

在上面我們說過,可以通過a[key]訪問到value,因此在本例中,我們類似的也用countFruit[fruit[i]]訪問到次數,但是需要注意的一點是,如果當前fruit[i]還不在map中,則建立新元素,且它的key為fruit[i],value為0。不管元素是否是新建立的,我們都將value加1。

最後的列印操作,我們使用了範圍for語句,訪問到map的每個元素(pair型別),然後列印他們的key和value。結果如下圖:

這裡寫圖片描述

上一個例子我們進行一個簡單的擴充套件,比如可能某種水果太貴了比如pear,watermelon等等,我們忽視這些輸入,我們則可以用set儲存要想要忽略的單詞,只對不在set集合的單詞統計次數。

map<string, size_t> countFruit;//定義map用來存取每種水果的次數
set<string> exclude = {"watermelon", "banana"};
for (size_t i = 0; i < n; ++i)
{
    if (exclude.find(fruit[i]) == exclude.end())
        ++countFruit[fruit[i]];
}

我們定義了一個set用來儲存需要忽略的元素,類似的set也是一個模板,我們需要傳入一個key型別。這個程式僅僅是多加了一句if,我們來看看是怎麼回事。我們呼叫它的find函式,它返回一個迭代器,如果給定key在set中,則迭代器指向該key,不然返回尾後迭代器以表示沒找到。這裡面我們就完成了僅當每種水果不在忽略集合中再統計它的次數。結果如下圖:

這裡寫圖片描述

看完了上面的例子,我們對map和set有了一個大致的瞭解,下面我們僅對一些常用操作進行介紹,其餘的可以參考C++文件查詢map和set完整詳細用法。

在上面,我們對map和set進行了一個說明,map是以key-value的形式存取,set以key形式存取,他們的底層都是以紅黑樹的結構實現,因此插入刪除等操作都在O(logn)時間內完成,因此可以完成高效的插入刪除。

由於是他們的底層是紅黑樹,因此在插入時候他們會預設執行排序操作,且他們key都是唯一的,因此從這個角度看,他們在某種程度上可以實現過濾重複值排序(預設升序)的功能。

在上面的統計水果次數例子中,對於重複水果,key = “apple”時對關鍵字沒有任何影響,但是可以改變value的值,這樣才實現了統計重複值的次數這一操作。另外注意我們用範圍for對map列印結果依次是

這裡寫圖片描述

這是按照字元ascii碼的順序升序得到的,因此有時候也可以讓map/set排序。

但是,如果想要改變預設次序,我們可以對map/set的建構函式傳入一個函式物件進行降序,下面我們來看看它的一些建構函式和拷貝建構函式。

bool fncomp (char lhs, char rhs) {return lhs<rhs;}

struct classcomp 
{
  bool operator() (const char& lhs, const char& rhs) const
  {return lhs<rhs;}
};

int main ()
{
    map<char, int> first;//預設升序 以key abcd 的順序儲存
    first['a'] = 10;
    first['b'] = 30;
    first['d'] = 50;
    first['c'] = 70;
    //注意:map的下標操作,其行為與vector很不相同:使用一個不在容器中關鍵字作為下標,會新增一個具有此關鍵字的元素到map中。一般使用find函式代替下標操作。
    map<char, int> second(first.begin(), first.end());

    map<char, int> third(second);

    map<char, int, classcomp> fourth; // 降序 以key dcba的順序儲存
    fourth['a'] = 10;
    fourth['b'] = 30;
    fourth['d'] = 50;
    fourth['c'] = 70;

    bool(*fn_pt)(char, char) = fncomp;
    map<char, int, bool(*)(char, char)> fifth(fn_pt); // function pointer as Compare
}

瞭解map構造,我們來看下最常用的insert和find操作, 在之前的統計水果次數題,我們插入元素使用[]操作,現在我們換種做法:

void CountFruitTimes(map<string, size_t>& countFruit, string fruit[], size_t n)
{
    for (size_t i = 0; i < n; ++i)
    {
        //auto ret = = countFruit.insert(make_pair(fruit[i], 1));
        pair<map<string, size_t>::iterator, bool> ret = countFruit.insert(make_pair(fruit[i], 1));
        if (!ret.second)
        {
            ret.first->second++;
        }
        //countFruit[fruit[i]]++;
    }
}

我們寫了個函式完成統計次數的功能,注意到insert返回的是pair型別,它的first為一個迭代器,second為bool型別,如果bool為true說明插入成功,此時它的key為fruit[i],value為1。如果bool為false說明插入失敗,說明該元素已經存在,但是我們可以通過insert返回值ret得到指向該元素的迭代器。

記得之前我們講過map裡面每個元素都是一個pair型別,在上面的例子中第一個為string,第二個是size_t,因此ret.first為迭代器訪問到size_t成員並對次數加1。

這樣就完成了統計次數的操作。