stl标准库系列之--map

1、概述

map是一个关联式容器,所谓的关联式容器,也就是类似与关联性数据库。每个元素都有一个键(key)和一个值(value)一一对应。关联式容器一般都是默认进行排序的。排序规则是按照键值的大小。容器的内部结构一般为 RB-tree,或者hashtable。

映射和多重映射基于某一类型Key的键集的存在,提供对T类型的数据进行快速和高效的检索。键和值的数据类型一般是不一致的。是一个pair类型中的两个分量。

所有类型的 map 容器保存的都是键值对类型的元素。map 容器的元素是 pair<const K,T> 类型的对象,这种对象封装了一个 T 类型的对象和一个与其关联的 K 类型的键。pair 元素中的键是 const,因为修改键会扰乱容器中元素的顺序。

2、pair

再看map的定义之前,我们先看看pair的定义,下面是<std_pair.h>中pair的定义:

template <class _T1, class _T2>
struct pair {
    typedef _T1 first_type;
    typedef _T2 second_type;

    _T1 first;
    _T2 second;
    pair() : first(_T1()), second(_T2()) {}
    pair(const _T1& __a, const _T2& __b) : first(__a), second(__b) {}

    #ifdef __STL_MEMBER_TEMPLATES
        template <class _U1, class _U2>
        pair(const pair<_U1, _U2>& __p) : first(__p.first), second(__p.second) {}
    #endif
};

上面的例子中我们需要注意的是:成员 first和second是public的。而我们经常会使用make_pair(const K&, const T&)来创建map,因此我们了解下这个函数。

template <class _T1, class _T2>
inline pair<_T1, _T2> make_pair(const _T1& __x, const _T2& __y)
{
    return pair<_T1, _T2>(__x, __y);
}

3、定义

看完pair的定义之后,我们看下map的定义,在SGI源码里面,map被定义为:

template <class _Key, class _Tp,                                //键和值
    class _Compare __STL_DEPENDENT_DEFAULT_TMPL(less<_Key>),    //默认缺省,并且采用递增排序(也就是键值的比较)
    class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >               //空间分配器
class map;

template <class _Key, class _Tp, class _Compare, class _Alloc>
class map {
public:

    // requirements:

    __STL_CLASS_REQUIRES(_Tp, _Assignable);
    __STL_CLASS_BINARY_FUNCTION_CHECK(_Compare, bool, _Key, _Key);

    // typedefs:

    typedef _Key                  key_type;         //键类型
    typedef _Tp                   data_type;        //值类型
    typedef _Tp                   mapped_type;      
    typedef pair<const _Key, _Tp> value_type;       //元素
    typedef _Compare              key_compare;      //键比较
        
    class value_compare : public binary_function<value_type, value_type, bool> {
        friend class map<_Key,_Tp,_Compare,_Alloc>;
    protected :
        _Compare comp;
        value_compare(_Compare __c) : comp(__c) {}
    public:
        bool operator()(const value_type& __x, const value_type& __y) const {
        return comp(__x.first, __y.first);
        }
    };

private:
    typedef _Rb_tree<key_type, value_type, 
                    _Select1st<value_type>, key_compare, _Alloc> _Rep_type;
    _Rep_type _M_t;  // red-black tree representing map

//下面是定义了一些指针、引用、迭代器等类型。
public:
    typedef typename _Rep_type::pointer pointer;
    typedef typename _Rep_type::const_pointer const_pointer;
    typedef typename _Rep_type::reference reference;
    typedef typename _Rep_type::const_reference const_reference;
    typedef typename _Rep_type::iterator iterator;
    typedef typename _Rep_type::const_iterator const_iterator;
    typedef typename _Rep_type::reverse_iterator reverse_iterator;
    typedef typename _Rep_type::const_reverse_iterator const_reverse_iterator;
    typedef typename _Rep_type::size_type size_type;
    typedef typename _Rep_type::difference_type difference_type;
    typedef typename _Rep_type::allocator_type allocator_type;
};

到此,SGI中map的定义已经结束了。我们能够清晰的看到,map实际上就是用了一个RB-tree来表现。需要注意的一点是:因为map是不允许键值重复的,因此一定是使用RB-tree底层的insert_unique,而非insert_equal。
在这里插入图片描述

4、特点

  • 自动建立K-T的对应。K和T的类型不作限制。
  • 快速查找,查找的时间复杂度为 log(N)
  • 只能同时存在一个相同的key
  • 默认是以键值的ASCII码大小排序

5、创建方法

stl中map有多种构造函数,我们先开看看他的多种构造函数。

在看构造方法之前,我们需要先了解一个概念:

#ifdef __STL_MEMBER_TEMPLATES

含义是:如果编译器支持template members of classes(模板类内嵌套模板) 就定义。

1、map<> 容器类的默认构造函数会创建一个空的 map 容器。

//construction
map() : _M_t(_Compare(), allocator_type()) {}
explicit map(const _Compare& __comp, const allocator_type& __a = allocator_type())
: _M_t(__comp, __a) {}

定义一个键和实值类型都为string类型的空map data。

std::map<std::string, std::string> data;    

map<K, T>中的每个元素都是同时封装了对象及其键的pair<const K, T>类型对象,因此不能修改const K。因此我们可以看作是data中的每个元素都是这样的 pair<const std::string, std::string>

2、以初始化列表的形式定义map对象

//construction
map(const value_type* __first, const value_type* __last) : _M_t(_Compare(), allocator_type())
{ _M_t.insert_unique(__first, __last); }

map(const value_type* __first, const value_type* __last, 
	const _Compare& __comp, const allocator_type& __a = allocator_type())
: _M_t(__comp, __a) { _M_t.insert_unique(__first, __last); }

理解这俩构造函数之前,我们先看一看 value_type的定义。从上面的map类的定义中我们能找到,typedef pair<const _Key, _Tp> value_type; 它其实是map中的每个元素。上面这两个构造函数的不同在于第二个指定了自己定义的排序函数。因此这中构造方法我们能够通过初始化列表的形式来看一下。

std::map<std::string, std::string> data {{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}};

手动修改自定义排序

std::map<std::string, std::string, std::greater<std::string> > data {{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}};

初始化列表中的值是通过将每个嵌套花括号中的两个值传递给构造函数产生的,因此列表会包含 4 个 pair<const std::string, std::string> 对象。

而我们前面已经知晓了pair 的定义和他的一个方法。 make_pair<T1, T2>() 模板。因此,上面的定义我们也可以写成是:

std::map<std::string, std::string> data {std::make_pair("a", "A"), std::make_pair("b", "B"), std::make_pair("c", "C"), std::make_pair("d", "D")};

同样的,下面的构造函数是以迭代器的形式出现的。

//construction
map(const_iterator __first, const_iterator __last) : _M_t(_Compare(), allocator_type()) 
    { _M_t.insert_unique(__first, __last); }

map(const_iterator __first, const_iterator __last, const _Compare& __comp, const allocator_type& __a = allocator_type())
    : _M_t(__comp, __a) { _M_t.insert_unique(__first, __last); }

也就是说,我们可以用另一个map的其中一段迭代器定义一个新的map,如下:

我们使用data的第二个元素到倒数第二个元素来创建一个新的map-data2.

std::map<std::string, std::string> data2 { ++std::begin(data), --std::end(data)};

在这边,我们还需要了解一下定义在宏__STL_MEMBER_TEMPLATES下面的两个构造函数:

template <class _InputIterator> map(_InputIterator __first, _InputIterator __last) : _M_t(_Compare(), allocator_type())
        { _M_t.insert_unique(__first, __last); }

    template <class _InputIterator> map(_InputIterator __first, _InputIterator __last, const _Compare& __comp, const allocator_type& __a = allocator_type())
        : _M_t(__comp, __a) { _M_t.insert_unique(__first, __last); }

这俩构造的功能和上面的两个是一样的,都是从一个map的部分区间来定义另外一个map。

3、复制构造

//construction
map(const map<_Key,_Tp,_Compare,_Alloc>& __x) : _M_t(__x._M_t) {}
map<_Key,_Tp,_Compare,_Alloc>&operator = (const map<_Key, _Tp, _Compare, _Alloc>& __x)
{
	_M_t = __x._M_t;
	return *this; 
}

复制构造是比较简单的定义的方法,如下:
我们使用data来创建一个新的map-data3

std::map<std::string, std::string> data3 {data}; 

需要注意的是,采用复制构造的map的compare必须和参数的相同。也就是说如果data自定义了排序函数,那么data2也必须使用相同的排序函数。

以上,我们通过map对象的构造函数来看了下怎么定义一个map。下面我们先通过一个简单的例子来看下上面的这几种构造方法,毕竟实验是检验真理的唯一有效途径。

    #include <iostream>
    #include <map>
    #include <string>

    using namespace std;

    int main(int argc, char* argv[])
    {
        map<string, string> data {make_pair("a", "A"), make_pair("b", "B"), make_pair("c", "C"), make_pair("d", "D")};
        for(map<string, string>::iterator itor = data.begin(); itor != data.end(); ++itor)
        {
            cout << itor->first << ' ' << itor->second << endl;
        }

        map<string, string> data2 { ++data.begin(), --data.end()};  //另一个map的部分迭代器区间
        for(map<string, string>::iterator itor = data2.begin(); itor != data2.end(); ++itor)
        {
            cout << itor->first << ' ' << itor->second << endl;
        }

        map<string, string> data3 {data};   //移动复制构造
        for(map<string, string>::iterator itor = data3.begin(); itor != data3.end(); ++itor)
        {
            cout << itor->first << ' ' << itor->second << endl;
        }

        //自定义排序函数
        map<string, string, greater<string>> data4 {{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}};
        for(map<string, string>::iterator itor = data4.begin(); itor != data4.end(); ++itor)
        {
            cout << itor->first << ' ' << itor->second << endl;
        }

        map<string, string, greater<string> > data5 {data4};
        for(map<string, string>::iterator itor = data5.begin(); itor != data5.end(); ++itor)
        {
            cout << itor->first << ' ' << itor->second << endl;
        }
        
        return 0;
}

运行结果如下:
在这里插入图片描述

6、成员函数

map的成员函数大多数都是已经被RB-tree 实现的,而map只是做了中间调用。

成员方法功能
find(key)在map容器中找键值为key的键值对是否存在,如果存在,返回指向该键值对的迭代器,如果不存在,则返回map最后一个元素所在位置的后一个位置的迭代器,如果map被const修饰,则迭代器也为const。
lower_bound(key)返回一个指向当前 map 容器中第一个大于或等于 key 的键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
upper_bound(key)返回一个指向当前 map 容器中第一个大于 key 的键值对的迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
equal_range(key)返回一个范围 pair<iterator,iterator>,包含两个迭代器,这个范围也就是lower_bound和upper_bound共同作用的区间。迭代器的区间是含有键值为key的键值对。map中key唯一,因此最多只有一个键值对。
empty()空 ?true :false
size()实际存有的键值对个数
max_size()返回 map 容器所能容纳键值对的最大个数,不同的操作系统,其返回值亦不相同。
operator[]重载[]运算符,下标为key。
at(key)找到 map 容器中 key 键对应的值,该函数会引起out_of_rang的异常(找不到的情况下)。
insert()向 map 容器中插入键值对。
erase()删除 map 容器指定位置、指定键(key)值或者指定区域内的键值对。后续章节还会对该方法做重点讲解。
swap()交换 2 个 map 容器中存储的键值对,这意味着,操作的 2 个键值对的类型必须相同。
clear()清空 map 容器中所有的键值对,即使 map 容器的 size() 为 0。
emplace()在当前 map 容器中的指定位置处构造新键值对。其效果和插入键值对一样,但效率更高。 返回值是一个pair<iterator,bool>对象,第一个值表示插入的位置,第二个表示是否成功
emplace_hint()在本质上和 emplace() 在 map 容器中构造新键值对的方式是一样的,不同之处,第一个参数必须时迭代器,指定在什么位置添加键值对,但好像没什么用,因为map的中排序函数会进行自动排序。
count(key)在当前 map 容器中,查找键为 key 的键值对的个数并返回。map中只返回0或1(键值对唯一)

7、迭代器

我们主要看下有关迭代器的成员函数,这些成员函数和上面的基本一样,已经被RB-tree实现,map只是做了过程调用。着重说下他的反向迭代器。rbegin();这是一个比较方便的能够遍历容器的方法,因为我曾经在vector上遍历的时候,没有记起来这个方法,导致从尾向前进行了迭代器的减法,以至于在过程中出现了一下不必要的麻烦。

成员方法功能
begin()返回指向容器中第一个键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
end()返回指向容器最后一个元素所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
rbegin()返回指向最后一个元素的反向双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。
rend()返回指向第一个元素所在位置前一个位置的反向双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。
cbegin()和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。
cend()和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。
crbegin()和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。
crend()和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的键值对。

迭代器的使用在每一个例子中都有用到。
在这里插入图片描述

8、几种插入数据的方法

1、重载运算符[]的使用

首先我们看下重载运算符[]在map中的使用。前面我们已经知道了,map重载了运算符[],带给我们的便利了就是我们能够像操作数组一样来操作map。我们通过一个简单的例子来看下:

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main(int argc, char* argv[])
{
    map<string, string> data {make_pair("a", "A"), make_pair("b", "B"), make_pair("c", "C"), make_pair("d", "D")};

    data["e"] = "E";

    cout << data["e"]  << "  " << data["f"] << endl;
    for(map<string, string>::iterator itor = data.begin(); itor != data.end(); ++itor)
    {
        cout << itor->first << ' ' << itor->second << endl;
    }

    return 0;
}

结果如下:
在这里插入图片描述
接下来我们看下在STL源码中是怎么定义重载运算符[]的。

_Tp& operator[](const key_type& __k) {
    iterator __i = lower_bound(__k);
    // __i->first is greater than or equivalent to __k.
    if (__i == end() || key_comp()(__k, (*__i).first))
    __i = insert(__i, value_type(__k, _Tp()));
    return (*__i).second;
}

看定义,如果能够找到键值为key的键值对,则返回该键值对的实值。如果找不到,则先插入一条空类型的键值对,然后返回该键值对的实值。

并且,该函数的返回值是引用,因此,我们可以通过赋值的方法来给他赋值。

2、insert函数的几种插入数据方法

1、无需指定插入位置,直接将键值对添加到 map 容器中

首先看下他的函数定义。

pair<iterator,bool> insert(const value_type& __x) 
  { return _M_t.insert_unique(__x); }

函数返回一个键值对pair<iterator,bool> 对象,第一个参数表示插入位置的迭代器,第二个表示是否插入成功,如果map中没有键值对__x,则插入成功,如果已经存在,则返回失败,返回的迭代器指向已经存在的键值对的位置迭代器。

std::map<std::string, std::string> data;
data.insert({"a", "A"});

2、指定插入位置

函数原型:

iterator insert(iterator position, const value_type& __x)
	{ return _M_t.insert_unique(position, __x); }

上面的这个函数则指定了插入的位置,返回值跟上面的差不多,返回的是插入之后或者已经存在的位置的迭代器。其实,指不指定位置都一样,如果指定了位置,会先按照指定的位置进行插入,插入之后,如果map底层发现破坏了他的有序性,则会触发自己的排序函数进行排序。

std::map<std::string, std::string> data;
data.insert(data.bengin(), {"a", "A"});

3、跟map的定义一样,插入另一个map的部分区间

void insert(const value_type* __first, const value_type* __last) {
    _M_t.insert_unique(__first, __last);
}
void insert(const_iterator __first, const_iterator __last) {
    _M_t.insert_unique(__first, __last);
}

上面的函数都是一样的操作,插入另一个map的部分。

std::map<std::string, std::string> data {{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}};
std::map<std::string, std::string> data2;
data2.insert(++data.bengin(), --data.end());

同样的,我们还需要了解一下定义在宏__STL_MEMBER_TEMPLATES下面的insert函数:

template <class _InputIterator>
void insert(_InputIterator __first, _InputIterator __last) {
	_M_t.insert_unique(__first, __last);
}

4、插入一个pair<K, T>的列表

跟声明map时一样,我们还可以插入一个pair<K, T>的列表。

std::map<std::string, std::string> data;
data2.insert({ {"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"} });

3、emplace函数

先来看下函数定义:

template<typename... _Args>
std::pair<iterator, bool>
emplace(_Args&&... __args)
{ return _M_t._M_emplace_unique(std::forward<_Args>(__args)...); }

参数 (Args&&… args) 指的是,这里只需要将创建新键值对所需的数据作为参数直接传入即可,此方法可以自行利用这些数据构建出指定的键值对。另外,该方法的返回值也是一个 pair 对象,其中 pair.first 为一个迭代器,pair.second 为一个 bool 类型变量:

  • 当该方法将键值对成功插入到 map 容器中时,其返回的迭代器指向该新插入的键值对,同时 bool 变量的值为 true;
  • 当插入失败时,则表明 map 容器中存在具有相同键的键值对,此时返回的迭代器指向此具有相同键的键值对,同时 bool 变量的值为 false。

4、emplace_hint函数

先来看下函数定义:

template<typename... _Args>
iterator
emplace_hint(const_iterator __pos, _Args&&... __args)
{
    return _M_t._M_emplace_hint_unique(__pos,
                    std::forward<_Args>(__args)...);
}

显然和 emplace 语法格式相比,有以下 2 点不同:

  • 该方法不仅要传入创建键值对所需要的数据,还需要传入一个迭代器作为第一个参数,指明要插入的位置(新键值对键会插入到该迭代器指向的键值对的前面);
  • 该方法的返回值是一个迭代器,而不再是 pair 对象。当成功插入新键值对时,返回的迭代器指向新插入的键值对;反之,如果插入失败,则表明 map 容器中存有相同键的键值对,返回的迭代器就指向这个键值对。

通过下面的例子来看下上面的这几种插入数据的方法。

#include <iostream>
#include <map>
#include <string>

using namespace std;

int main(int argc, char* argv[])
{
    map<string, string> data {make_pair("a", "A"), make_pair("b", "B"), make_pair("c", "C"), make_pair("d", "D")};

    data["e"] = "E";
    cout << data["e"]  << "  " << data["f"] << endl;
    for(map<string, string>::iterator itor = data.begin(); itor != data.end(); ++itor)
    {
        cout << itor->first << ' ' << itor->second << " ";
    }
    cout << endl;

    data.insert({"g", "G"});
    data.insert(++data.begin(), {"h", "H"});
    data.insert({{"i", "I"}, {"j", "J"}});

    auto pr = data.emplace("k", "K");
    data.emplace_hint(pr.first, "l", "L");

    for(map<string, string>::iterator itor = data.begin(); itor != data.end(); ++itor)
    {
        cout << itor->first << ' ' << itor->second << " ";
    }
    cout << endl;
    
    map<string, string> data2;
    data2.insert(++data.begin(), --data.end());
    for(map<string, string>::iterator itor = data2.begin(); itor != data2.end(); ++itor)
    {
        cout << itor->first << " " << itor->second << " ";
    }

    return 0;
}

结果如下:
在这里插入图片描述

9、insert、emplace和emplace_hint

上面我们看了插入数据的几中方法,其中主要的有3个成员函数,那么,这3个函数之间有什么区别呢?我们主要通过下面的一个例子来看下:

#include <iostream>
#include <map>
#include <string>
using namespace std;

class MapValue{
public:
    MapValue(int num) : m_number(num) {
        std::cout << "construction..." << endl;
    }
    MapValue(MapValue&& other) : m_number(other.m_number) {
        std::cout << "move construction..." << endl;
    }
private:
    int m_number;
};

int main(int argc, char* argv[])
{
    map<string, MapValue> data;
// 分为两种情况来看
//    cout << "insert..." << endl;
//    data.insert({"a", MapValue(1)});
//
//    cout << "emplace..." << endl;
//    data.emplace("b", MapValue(1));
//
//    cout << "emplace_hint..." << endl;
//    data.emplace_hint(data.begin(), "c", MapValue(1));

    cout << "insert..." << endl;
    data.insert({"a", 1});

    cout << "emplace..." << endl;
    data.emplace("b", 1);

    cout << "emplace_hint..." << endl;
    data.emplace_hint(data.begin(), "c", 1);
    return 0;
}

结果如下:
1、先看下注释调的部分
在这里插入图片描述
2、再看下未注释的部分
在这里插入图片描述

从上面的结果来看,(以注释掉的代码运行结果做分析)调用后insert函数时,程序总共调用了类的构造函数一次、移动构造函数两次。而emplace和emplace_hint函数都只调用了一次构造和一次移动构造

如果我们让编译器自己做数据类型推导时发现,都会少一次移动构造。
缺少的是在最后插入的一次移动构造。

可以得出结论:

emplaceemplace_hint 在效率上明显高于insert方法。所以在map插入数据时,尽量使用emplaceemplace_hint 代替 insert

上面insert方法的三次调用我们可以看作是:

MapValue val = MapValue(1);
auto pair = make_pair<"a", val>;
data.insert(pair);
  • 首先,调用构造函数,构建了类MapValue 的临时变量val
  • 其次,调用std::make_pair<T1, T2>模板形成pair对象pair,调用一次移动构造函数
  • 最后,调用data::insert(pair<>)进行数据插入,调用一次移动构造

以上,stl-map基本上已经学完了。
会用-明理-掌握

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值