STL之Map

概述

Map是標準關聯式容器associative container)之一,一個map是一個鍵值對序列,即(key ,value)對。它提供基於key的快速檢索能力,在一個map中key值是唯一的。map提供雙向迭代器,即有從前往後的(iterator),也有從後往前的(reverse_iterator)。

map要求能對key進行<操作,且保持按key值遞增有序,因此map上的迭代器也是遞增有序的。如果對於元素並不需要保持有序,可以使用hash_map

map中key值是唯一的,如果馬匹中已存在一個鍵值對(暱稱,密碼):("skynet",407574364),而我們還想插入一個鍵值對("skynet",472687789)則會報錯不是報錯,準確的說是,返回插入不成功!)。而我們又的確想這樣做,即一個鍵對應多個值,幸運的是multimap可是實現這個功能。

下面我們用實例來深入介紹mapmultimap,主要內容如下:

  • 1、例子引入
  • 2、map中的類型定義
  • 3、map中的迭代器和鍵值對
  • 4、map中的構造函數與析構函數
  • 5、map中的操作方法
  • 6、再議map的插入操作
  • 7、[]不僅插入
  • 8、multimap
  • 9、總結

1、例子引入

有一個服務器manager維護着接入服務器的client信息,包括clinetId、scanRate、socketAddr等等。我們定義一個結構體保存scanRate、socketAddr信息。如下:

1
2
3
4
5
typedef    int    clientId;
typedef struct{
    int scanRate;
    string socketAddr;
}clientInfo;

我們用map保存這些信息:clientId爲鍵key,clientInfo爲值。這樣我們可以通過clientId快速檢索到client的相關信息,我們可以這樣定義:

1
map<clientId,clientInfo> clientMap;

這樣我們定義了一個clientMap,如果我們要定義多個這樣的map,需要多次寫map<clientId,clientInfo> 變量名。爲了避免這樣情況,我們通常爲map<clientId,clientInfo>定義個別名,如:

1
2
typedef map<clientId,clientInfo> clientEdp;
clientEdp clientMap;

之後我們就可以像定義clientMap一樣定義map<clientId,clientInfo>對象,這樣的好處還有:如果我們需要修改map的定義,只需要在一處修改即可,避免修改不徹底造成的不一致現象。

我們這就完成了需要的map的定義,如果不定義或沒有在它上面的操作的話,就像定義類而沒有方法一樣,意義不大或毫無意義。幸運的是,STL提供了這些常用操作:排序(注:map是不能也不要排序的,因爲map本身已經排好序了)、打印、提取子部分、移除元素、添加元素、查找對象,就像數據庫的增刪改查操作!現在我們詳細介紹這些操作,並逐步引入hash_mapmultimap

2、map中的類型定義

關聯數組(associative array)是最有用的用戶定義類型之一,經常內置在語言中用於文本處理等。一個關聯數組通常也稱爲map,有時也稱字典(dictionary),保存一對值。第一個值稱爲key、第二個稱爲映射值mapped-value。

標準map是定義在std命名空間中的一個模板,並表示爲<map>。它首先定義了一組標準類型名字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
template<class Key,class T,class Cmp=less<key>,
    class A=allocator<pair<const Key,T>>
class std::map
{
public:
    //types
    typedef Key    key_type;
    typedef T    mapped_type;
 
    typedef pair<const Key,T>    value_type;
 
    typedef    Cmp    key_compare;
    typedef A    allocator_type;
 
    typedef    typename    A::reference    reference;
    typedef    typename    A::const_reference    const_reference;
 
    typedef    implementation_define1    iterator;
    typedef implementation_define2    const_iterator;
 
    typedef    typename    A::size_type    size_type;
    typedef    typename    A::difference_type    difference_type;
 
    typedef    std::reverse_iterator<iterator>    reverse_iterator;
    typedef    std::reverse_iterator<const_iterator>    const_reverse_iterator;
    //...
}

注意:map的value_type是一個(key,value)對,映射值的被認爲是mapped_type。因此,一個map是一個pair<const Key,mapped_type>元素的序列。從const Key可以看出,map中鍵key是不可修改的。

不得不提的是map定義中Cmp和A都是可選項。Cmp是定義在元素之間的比較方法,默認是<操作;A即allocator用來分配釋放map總鍵值對所需使用的內存,沒有指定的話即默認使用的是STL提供的,也可以自定義allocator來管理內存的使用。多數情況,我們不指定這兩個選項而使用默認值,這樣我們定義map就像下面這樣:

1
map<int,clientInfo> clientMap;

Cmp和A都缺省。 通常,實際的迭代器是實現定義的,因爲map很像使用了樹的形式,這些迭代器通常提供樹遍歷的某種形式。逆向迭代器是使用標準的reverse_iterator模板構造的。

3、map中的迭代器和鍵值對

map提供慣常的返回迭代器的一組函數,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<class Key,class T,class Cmp=less<key>,
    class A=allocator<pair<const Key,T>>
class std::map
{
public:   
    //...
    //iterators
    iterator    begin();
    const_iterator    begin()    const;
 
    iterator    end();
    const_iterator    end()    const;
 
    reverse_iterator    rbegin();
    const_reverse_iterator    rbegin()    const;
 
    reverse_iterator    rend();
    const_reverse_iterator    rend()    const;
    //...
}

map上的迭代器是pair<const Key,mapped_type>元素序列上簡單的迭代。例如,我們可能需要打印出所有的客戶端信息,像下面的程序這樣。爲了實現這個,我們首先向《例子引入》中定義的clientEdp中插入數據,然後打印出來:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<iostream>
#include<map>
#include<string>
using namespace std;
 
typedef    int    clientId;
typedef struct{
    int scanRate;
    string socketAddr;
}clientInfo;
 
int main(int argc,char** argv)
{
    typedef map<clientId,clientInfo> clientEdp;
    typedef map<clientId,clientInfo>::const_iterator iterator;
 
    clientEdp clients;
    clientInfo client[100];
    char str[10];
    string strAddr("socket addr client ");
 
    for(int i=0;i<100;i++)
    {
        client[i].scanRate=i+1;   
        //convert int to char*
        itoa(i+1,str,10);
        //concatenate strAddr and str
        client[i].socketAddr=strAddr+str;
        cout<<client[i].socketAddr<<endl;
        clients.insert(
            make_pair(i+1,client[i]));   
    }
    delete str;
    for(iterator i=clients.begin();i!=clients.end();i++)
    {
        cout<<"clientId:"<<i->first<<endl;
        cout<<"scanRate:"<<i->second.scanRate<<endl;
        cout<<"socketAddr:"<<i->second.socketAddr<<endl;
        cout<<endl;
    }
}

一個map迭代器以key升序方式表示元素,因此客戶端信息以cliendId升序的方式輸出。運行結果可以證明這一點,運行結果如下所示:

image

圖1、程序運行結果

我們以first引用鍵值對的key,以second引用mapped value,且不用管key和mapped value是什麼類型。其實pair在std的模板中是這樣定義的:

1
2
3
4
5
6
7
8
9
10
11
12
template <class    T1,class T2>struct std::pair{
    typedef    T1    first_type;
    typedef    T2    second_type;
 
    T1    first;
    T2    second;
 
    pair():first(T1()),second(T2()){}
    pair(const T1& x,const T2& y):first(x),second(y){}
    template<class U,class V>
    pair(const pair<U,V>& p):first(p.first),second(p.second){}
}

即map中,key是鍵值對的第一個元素且mapped value是第二個元素。pair的定義可以在<utility>中找到,pair提供了一個方法方便創建鍵值對:

1
2
3
4
5
template <class T1,class T2>pair<T1,T2>
    std::make_pair(const T1& t1,const T2& t2)
{
    return pair<T1,T2>(t1,t2);
}

上面的例子中我們就用到了這個方法來創建(clientId,clientInfo)對,並作爲Insert()方法的參數。每個pair默認初始化每個元素的值爲對應類型的默認值。

4、map中的構造函數與析構函數

map類慣常提供了構造函數和析構函數,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<class Key,class T,class Cmp=less<key>,
    class A=allocator<pair<const Key,T>>
class std::map
{
    //...
    //construct/copy/destroy
    explicit map(const Cmp&=Cmp(),const A&=A());
    template<class In>map(In first,In last,
        const Com&=Cmp(),const A&=A());
    map(const map&);
 
    ~map();
 
    map& operator=(const map&);
    //...
}

複製一個容器意味着爲它的每個元素分配空間,並拷貝每個元素值。這樣做是性能開銷是很大的,應該僅當需要的時候才這樣做。因此,map傳的是引用

5、map中的操作方法

前面我們已經說過,如果map中僅定義了一些key、mapped value類型的信息而沒有操作方法,就如定義個僅有字段的類意義不大甚至毫無意義。由此可見map中定義操作方法非常重要!前面的例子我們就用到了不少方法,如返回迭代器的方法begin()、end(),鍵值對插入方法insert()。下面我們對map中的操作方法做個全面的介紹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
template<class Key,class T,class Cmp=less<key>,
    class A=allocator<pair<const Key,T>>
class std::map
{
    //...
    //map operations
 
    //find element with key k
    iterator find(const key_type& k);
    const_iterator find(const key_type& k) const;
 
    //find number of elements with key k
    size_type count() const;
 
    //find first element with key k
    iterator lower_bound(const key_type& k);
    const_iterator lower_bound(const key_type& k) const;
 
    //find first element with key greater than k
    iterator upper_bound(const key_type& k);
    const_iterator upper_bound(const key_type& k) const;
 
    //insert pair(key,value)
    pair<iterator,bool>insert(const value_type& val);
    iterator insert(iterator pos,const value_type& val);
    template<class In>void insert(In first,In last);
 
    //erase element
    void erase(iterator pos);
    size_type erase(const key_type& k);
    void erase(iterator first,iterator last);
    void clear();
 
    //number os elements
    size_type size() const;
 
    //size of largest possible map
    size_type max_size() const;
 
    bool empty() const{return size()==0;}
 
    void swap(map&);
    //...
}

上面這些方法基本都能顧名思義(PS.由此可見,命名有多重要,我們平時要養成好的命名習慣,當然註釋也必不可少!)。雖然已經非常清楚了了,但我還是想講解一下以消除不惜要的誤解和更好地應用這些方法。

  • find(k)方法簡單地返回鍵值爲k的元素的迭代器;如果沒有元素的鍵值爲k,則返回map的end()迭代器。由於map是按鍵key升序排列,所有查找的複雜度只有O(logN)。因此,我們通常會這樣用這個方法: 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    #include<iostream>
    #include<map>
    #include<string>
    using namespace std;
     
    typedef    int    clientId;
    typedef struct{
        int scanRate;
        string socketAddr;
    }clientInfo;
     
    int main(int argc,char** argv)
    {
        typedef map<clientId,clientInfo> clientEdp;
        typedef map<clientId,clientInfo>::const_iterator iterator;
     
        clientEdp clients;
        clientInfo client[100];
        char* str=new char[10];
        string strAddr("socket addr client ");
     
        for(int i=0;i<100;i++)
        {
            client[i].scanRate=i+1;   
            //convert int to char*
            itoa(i+1,str,10);
            //concatenate strAddr and str
            client[i].socketAddr=strAddr+str;
            clients.insert(
                make_pair(i+1,client[i]));   
        }
        delete str;
    <span style="color: #ff0000;">    </span><b><span style="color: #ff0000;">clientId id=10;
        iterator i=clients.find(id);
        if(i!=clients.end()){
            cout<<"clientId: "<<id
                <<" exists in clients"<<endl;
        }
        else{
            cout<<"clientId: "<<id
                <<" doesn't exist in clients"<<endl;
        }</span></b>   
    }
  • insert()方法 試圖將一個(Key,T)鍵值對加入map。因爲鍵時唯一的,所以僅當map中不存在鍵值爲k的鍵值對時插入才成功。該方法的返回值爲pair<iterator,bool>,如果插入成功bool值爲TRUE,iterator指向插入map中後的鍵值對。如下代碼: 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    #include<iostream>
    #include<map>
    #include<string>
    using namespace std;
     
    typedef    int    clientId;
    typedef struct{
        int scanRate;
        string socketAddr;
    }clientInfo;
     
    int main(int argc,char** argv)
    {
        typedef map<clientId,clientInfo> clientEdp;
        typedef map<clientId,clientInfo>::const_iterator iterator;
     
        clientEdp clients;
     
        clientId id=110;
        clientInfo cltInfo;
        cltInfo.scanRate=10;
        cltInfo.socketAddr="110";
        pair<clientId,clientInfo> p110(id,cltInfo);
        pair<iterator,bool> p=clients.insert(p110);
        if(p.second){
            cout<<"insert success!"<<endl;
        }
        else{
            cout<<"insert failed!"<<endl;
        }   
        //i points to clients[110];
        iterator i=p.first;
        cout<<i->first<<endl;
        cout<<i->second.scanRate<<endl;
        cout<<i->second.socketAddr<<endl;
    }

上面我們看出,這裏我們插入鍵值對是首先聲明一個鍵值對pair<clientId,clientInfo> p110(id,cltInfo); 然後再插入,這個我們之前make_pair方法不一樣,make_pair方法用的比較多。

  • erase()方法用法比較簡單,比如像清除clientId爲110的鍵值對,我們只需要對clients調用erase方法:clients.erase(clients.find(110));或者我們想清除clientId從1到10的鍵值對,我們可以這樣調用erase()方法:clients.erase(clients.finds(1),clients.find(10));簡單吧!別得意,你還需要注意,如果find(k)返回的是end(),這樣調用erase()方法則是一個嚴重的錯誤,會對map造成破壞操作。

6、再議map的插入操作

前面我們介紹了利用map的插入方法insert(),聲明鍵值對pair或make_pair生成鍵值對然後我們可以輕鬆的將鍵值對插入map中。其實map還提供了更方便的插入操作利用下標(subscripting,[])操作,如下:

1
2
3
4
5
clientInfo cltInfo;
cltInfo.scanRate=10;
cltInfo.socketAddr="110";
 
<b>clients[110]=cltInfo;</b>

這樣我們就可以簡單地將鍵值對插入到map中了。下標操作在map中式這樣定義的:

1
2
3
4
5
6
7
8
9
template<class Key,class T,class Cmp=less<key>,
    class A=allocator<pair<const Key,T>>
class std::map
{
    //...
    //access element with key k
    mapped_type& operator[](const key_type& k);
    //...
}

我們來分析一下應用[]操作,插入鍵值對的過程:檢查鍵k是否已經在map裏。如果不,就添加上,以v作爲它的對應值。如果k已經在map裏,它的關聯值被更新成v。這裏首先,查找110不在map中則創建一個鍵爲110的鍵值對,並將映射值設爲默認值,這裏scanRate爲0,socketAddr爲空;然後將映射值賦爲cltInfo。 如果110在map中已經存在的話,則只是更新以110爲鍵的映射值。

從上面的分析可知:如果大量這樣插入數據,會嚴重影響效率!如果你考慮效率問題,請使用insert操作。insert方法,節省了三次函數調用:一個建立臨時的默認映射值的對象,一個銷燬那個臨時的對象和一個對映射值的賦值操作。

Note1:如果k已經存在map中,[]效率反而比insert的效率高,而且更美觀!如果能夠兼顧這兩者那豈不是很美妙!其實我們重寫map中的[]操作:首先判斷k是否已經在map中,如果沒有則調用insert操作,否則調用內置的[]操作。如下列代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//////////////////////////////////////////////
///@param MapType-map的類型參數
///@param KeyArgType-鍵的類型參數
///@param ValueArgtype-映射值的類型參數
///@return 迭代器,指向鍵爲k的鍵值對
//////////////////////////////////////////////
template<typename MapType,
        typename KeyArgType,
        typename ValueArgtype>
typename MapType::iterator
    efficientAddOrUpdate(MapType& m,
            const KeyArgType& k,
            const ValueArgtype& v)
{
    typename MapType::iterator Ib =    m.lower_bound(k);
    if(Ib != m.end()&&!(m.key_comp()(k,Ib->first))) {
        //key已經存在於map中做更新操作
        Ib->second = v;   
        return Ib;           
    }
    else{
        //key不存在map中做插入操作
        typedef typename MapType::value_type MVT;
        return m.insert(Ib, MVT(k, v));   
    }                   
}

Note2:我們視乎還忽略了一點,如果映射值mapped value的類型沒有默認值,怎麼辦?這種情況請勿使用[]操作插入。

7、[]不僅插入

通過[]操作不僅僅是插入鍵值對,我們也可以通過鍵key檢索出映射值mapped value。而且我們利用[]操作可以輕鬆地統計信息,如有這樣這樣一些鍵值對(book-name,count)對:

(book1,1)、(book2,2)、(book1,2)、(book3,1)、(book3,5)

我們計算每種book的數量總和。我們可以這樣做:將它們讀入一個map<string,int>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
#include<map>
#include<string>
using namespace std;
 
int main(int argc,char** argv)
{
    map<string,int> bookMap;
 
    string book;
    int count;
    int total=0;
    while(cin>>book>>count)
        bookMap[book]+=count;
 
    map<string,int>::iterator i;
    for(i=bookMap.begin();i!=bookMap.end();i++)
    {
        total+=i->second;
        cout<<i->first<<'\t'<<i->second<<endl;
    }
    cout<<"total count:"<<total<<endl;
}

結果如下所示:(注意按住ctrl+z鍵結束輸入)

image

圖2、程序運行結果

8、multimap

前面介紹了map,可以說已經非常清晰了。如果允許clientId重複的話,map就無能爲力了,這時候就得multimap上場了!multimap允許鍵key重複,即一個鍵對應多個映射值。其實除此之外,multimap跟map是很像的,我們接下來在map的基礎上介紹multimap。

multimap在std中的定義跟map一樣只是類名爲multimap,multimap幾乎有map的所有方法和類型定義。

  • multimap不支持[]操作;但map支持
  • multimap的insert方法返回的是一個迭代器iterator,沒有bool值;而map值(iterator,bool)的元素對
  • 對應equal_range()、方法:
    pair<iterator,iterator> equal_range(const key_type& k);
    pair<const_iterator,const_iterator>
    	equal_range(const key_type& k) const;
    
    //find first element with key k
    iterator lower_bound(const key_type& k);
    const_iterator lower_bound(const key_type& k) const;
    
    //find first element with key greater than k
    iterator upper_bound(const key_type& k);
    const_iterator upper_bound(const key_type& k) const;
    雖然在map和multimap都有,顯然對multimap有更多的意義!equal_range()方法返回一個鍵key對應的多個映射值的上界和下界的鍵值對的迭代器、lower_bound()方法返回鍵multimap中第一個箭爲key的鍵值對迭代器、upper_bound()方法返回比key大的第一個鍵值對迭代器。

假設我們想取出鍵爲key的所有映射值,我們可以這樣做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include<iostream>
#include<map>
#include<string>
using namespace std;
 
typedef int clientId;
typedef struct{
    int scanRate;
    string socketAddr;
}clientInfo;
 
int main(int argc,char** argv)
{
    typedef multimap<clientId,clientInfo> clientEdp;
    typedef multimap<clientId,clientInfo>::const_iterator iterator;
     
    clientEdp clients;
    clientInfo client[20];
    char* str=new char[10];
    string strAddr("socket addr client ");
 
    for(int i=0;i<10;i++)
    {
        client[i].scanRate=i+1;   
        //convert int to char*
        itoa(i+1,str,10);
        //concatenate strAddr and str
        client[i].socketAddr=strAddr+str;
        clients.insert(
            make_pair(10,client[i]));   
    }
    for(int i=10;i<20;i++)
    {
        client[i].scanRate=i+1;   
        //convert int to char*
        itoa(i+1,str,10);
        //concatenate strAddr and str
        client[i].socketAddr=strAddr+str;
        clients.insert(
            make_pair(i+1,client[i]));   
    }
    delete str,strAddr;
     
<b>    //find elements with key 10
    iterator lb=clients.lower_bound(10);
    iterator ub=clients.upper_bound(10);</b>
    for(iterator i=lb;i!=ub;i++)
    {
        cout<<"clientId:"<<i->first<<endl;
        cout<<"scanRate:"<<i->second.scanRate<<endl;
        cout<<"socketAddr:"<<i->second.socketAddr<<endl;
        cout<<endl;
    }
}

(說明:實際上,一般是不允許clientId重複的,這裏只是爲了舉例。)這樣是不是感覺很醜呢!事實上,我們可以更簡單的這樣:

1
2
3
4
5
6
7
8
9
//find elements with key 10
<b>pair<iterator,iterator> p=clients.equal_range(10);</b>
for(iterator i=p.first;i!=p.second;i++)
{
    cout<<"clientId:"<<i->first<<endl;
    cout<<"scanRate:"<<i->second.scanRate<<endl;
    cout<<"socketAddr:"<<i->second.socketAddr<<endl;
    cout<<endl;
}

總結

map是一類關聯式容器。它的特點是增加和刪除節點對迭代器的影響很小,除了那個操作節點,對其他的節點都沒有什麼影響。對於迭代器來說,可以修改實值,而不能修改key。

map的功能:

  • 自動建立Key -value的對應。key 和value可以是任意你需要的類型。
  • 根據key值快速查找記錄,查找的複雜度基本是Log(N)。
  • 快速插入Key - Value 記錄。
  • 快速刪除記錄
  • 根據Key 修改value記錄。
  • 遍歷所有記錄。

展望:本文不知不覺寫了不少字了,但仍未深入涉及到map定義的第3個和第4個參數,使用的都是默認值。

template<class Key,class T,class Cmp=less<key>, 
    class A=allocator<pair<const Key,T>>

感興趣者,請查找相關資料or下面留言希望看到單獨開篇介紹map第3個和第4個參數。您的支持,我的動力!PS:在此文的原因,在與公司做項目用到了map特此總結出來與大家共享,不過在進行個人總結過程中,難免會有疏漏或不當之處,請不吝指出。

參考文獻:

【1】《The C++ Programming Language (Special Edition)》

【2】《Effective STL》


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章