map和set的使用和原理

我们学习过顺序容器如vecor,list等,它们中的元素是按照在容器中的位置来顺序保存和访问的。而接下来要学习的关联容器则有根本的不同,它们中的元素是按关键字来保存和访问的。
在《C++Primer》中列举了标准库中的8个关联容器,如下:

map和set的使用和原理_第1张图片

关联容器支持高效的关键字查找和访问,我们在这里介绍两个主要的关联容器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。结果如下图:

map和set的使用和原理_第2张图片

上一个例子我们进行一个简单的扩展,比如可能某种水果太贵了比如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的使用和原理_第3张图片



看完了上面的例子,我们对map和set有了一个大致的了解,下面我们仅对一些常用操作进行介绍,其余的可以参考C++文档查询map和set完整详细用法。

在上面,我们对map和set进行了一个说明,map是以key-value的形式存取,set以key形式存取,他们的底层都是以红黑树的结构实现,因此插入删除等操作都在O(logn)时间内完成,因此可以完成高效的插入删除。

由于是他们的底层是红黑树,因此在插入时候他们会默认执行排序操作,且他们key都是唯一的,因此从这个角度看,他们在某种程度上可以实现过滤重复值排序(默认升序)的功能。

在上面的统计水果次数例子中,对于重复水果,key = “apple”时对关键字没有任何影响,但是可以改变value的值,这样才实现了统计重复值的次数这一操作。另外注意我们用范围for对map打印结果依次是

map和set的使用和原理_第4张图片

这是按照字符ascii码的顺序升序得到的,因此有时候也可以让map/set排序。

但是,如果想要改变默认次序,我们可以对map/set的构造函数传入一个函数对象进行降序,下面我们来看看它的一些构造函数和拷贝构造函数。

bool fncomp (char lhs, char rhs) {return lhsstruct classcomp 
{
  bool operator() (const char& lhs, const char& rhs) const
  {return lhsint 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。

这样就完成了统计次数的操作。

你可能感兴趣的:(数据结构)