本文是玩转C++11的map的单独文章,更多内容可以点击链接跳转
文章目录
map——映射
map使用的是红黑树用于查找,封装在头文件map中。
先用代码说说map是怎么使用的吧:
#include<map>
std::map<key,value> temp = {key1,value1};
map的元素为pair,它跟map有点像:
std::pair<key,value> temp = {key1,value1};
并且,map也提供了迭代器,由于其内部的实现使用的是双向链表,因此其迭代器也是双向迭代器。
在先前我们已经说了map的底层是基于红黑树实现的,因此存进map的值需要能够明确地进行大小比较。所以在使用自定义类型的时候,如果要让我们的自定义数据类型作为map的键,至少需要重载<运算符。
(1) map的基本操作
方法 | 作用 | 时间复杂度 |
---|---|---|
insert({key , value}) | 向map中插入一个键值对<key,value> | O(logn) |
erase(key) | 删除map中的指定的键值对 | O(logn) |
find(key) | 查找map中指定键对应的键值对的迭代器 | O(logn) |
count(key) | 查找键的数量,在普通map中,由于键唯一,故只返回0、1 | O(logn) |
operator[key] | 查找map中指定键对应的值 | O(logn) |
size() | 返回map中键值对的数量 | O(1) |
clear() | 清空map中所有的键值对 | O(n) |
empty() | 判断map是否为空 | O(1) |
begin() | 返回map中第一个键值对的迭代器 | O(1) |
cbegin() | 前者的const版本 | |
rbegin() | 指向“起始”的逆向迭代器 | |
end() | 返回map中最后一个键值对的下一个位置的迭代器 | O(1) |
cend() | 前者的const版本 | |
rend() | 指向“尾节点的下一个节点“的逆向迭代器 | |
emplace() | 原地构造函数 |
(2) pair——map的元素
经过上面的表述,已经知道了pair是map的元素,它储存的是键值对,根据所在位置的不同我们可以使用first和second来分别表示key和value:
std::pair<const std::string,int> student("西施",18);
std::cout << "student.first = " << student.first << '\n';
std::cout << "student.second = " << student.second;
所以我们想要操作map其实是对pair的一系列操作。
map有三种额外类型:key_type,mapped_type和value_type:
额外类型 | 它是什么? |
---|---|
key_type | 此容器的关键字类型,也就是key |
mapped_type | 关键字的关联类型,也就是value |
value_type | map的类型,其实就是pair<const key_type, mapped_type> |
可能会注意到:为什么map的value_type的键使用了const修饰?这是因为map的键是不允许修改的,将其修饰为const也就避免了误操作。
(5) map的迭代器
map也是支持迭代器的,跟大多数STL中的模板类一样,虽然map的元素是pair,但是它的迭代器并不是pair类型,而是:
std::map<key_type, mapped_type>::iterator
这个也很好理解,我也就不再赘述了。
(4) insert()
insert()有很多需要注意的地方,我们先看看insert的声明吧:
//实际上有很多,不止这一个
pair<iterator, bool> insert(const value_type& value);
根据先前的内容,可以知道insert()传入的是一个pair,同时它返回一个pair,但是返回的这个pair并不是之前传入的pair了!那么这个pair究竟是什么呢?
它的first是个迭代器,second是一个布尔值,那么问题来了:first的迭代器指向谁呢?这里用一段代码来测试下:
std::map<std::string, int> girl = {};
std::pair<std::string, int> temp = { "西施", 18 };
auto res = girl.insert(temp);
std::cout << "res.first->first = " << res.first->first << "\n";
std::cout << "res.first->second = " << res.first->second;
输出的结果为:
西施 18
并且可以得到res的类型为:std::pair<std::map<std::string, int>::iterator, bool>。
因此,可以得出结论:如果insert()插入成功,这个迭代器将会指向这个成功插入的元素,同时bool设为true;若是插入失败,说明在map中已有这个关键字,则会指向map中,键与想要插入的元素的键相同的对象,bool设为false。
在上面的代码中,我们是创建了一个std::pair的变量后,再将其用于insert()函数传参,但是更多情况下,我们是使用std::pair的非成员函数:std::make_pair(),以此来避免创建多余的变量占用栈的空间,对上面的代码进行修改:
std::map<std::string, int> girl = {};
//make_pair不是pair的成员函数
//所以需要使用std来指定命名空间
auto res = girl.insert(std::make_pair("西施", 18));
std::cout << "res.first->first = " << res.first->first << "\n";
std::cout << "res.first->second = " << res.first->second;
这才是我们更常用的形式。
(5) find()与count()
find和count都是map中的查找元素的函数,但是它们的作用不太一样,我们分别看看二者的声明:
iterator find(const Key& key);
size_type count(const Key& key) const;
可以发现,虽然二者都起到了查找的作用,但是二者的返回值截然不同:前者是一个迭代器而后者是一个size_type(可以说就是size_t,一种无符号整型);find()返回的迭代器就是我们根据键值查到的那一个,如果它不在map中,则返回的迭代器等同于end(),由于在map中,每个键只对应一个值,所以count()的返回值只有0、1两种可能。
(6) erase()
这是删除函数,能够删除指定的元素或指定范围内的元素。照例先看声明:
size_type erase(const Key& key);
iterator erase(iterator pos);
iterator erase(iterator first, iterator last);
这里我们写一段代码,试试第二种删除(因为第一种很好理解,第三种跟第二种本质上是一样的):
#include<iostream>
#include<map>
#include<iterator>
int main() {
std::map<std::string, int> gl = {
{"西施",18},
{"貂蝉",19},
{"如花",20}
};
//用auto会省事很多,我这么写是为了能更好地体现它的数据类型
//target指向要删除的元素
std::map<std::string, int>::iterator target = gl.begin();
std::cout << target->first << " " << target->second << "\n";
//temp是指向target的下一个元素的迭代器
auto temp = target;
temp++;
std::cout << temp->first << " " << temp->second << "\n";
//erase_res接收删除target之后返回的迭代器
std::map<std::string, int>::iterator erase_res = gl.erase(target);
std::cout << erase_res->first << " " << erase_res->second << "\n";
}
我们看看运行的结果是什么:
如花 20
西施 18
西施 18
可以发现,temp和erase_res指向同一个元素。
总结出来就是它有三个类型,也就是上述,我们一个个进行分析:
- 传入一个键,删除这个键所对应的键值对,返回一个size_type,表示删除元素的个数,在普通map中只可能是0、1。
- 传入一个迭代器,删除指定的迭代器,返回被删除元素的下一个元素的迭代器。
- 传入两个迭代器,删除这两个迭代器之间的元素(包括第一个迭代器和最后一个迭代器指向的元素),并返回删除后的下一个元素的迭代器。
(7) 随机访问[]
把它单独提出来肯定是它有特别的地方。在数组中我们就支持随机访问(即下标访问),map它也支持,并且访问方式很简单:跟数组差不多,只是将里面的元素变为map的键,这里给个实例:
#include<iostream>
#include<map>
int main(){
std::map<std::string, int> gl = {
{"西施",18},
{"貂蝉",19},
{"如花",20}
};
std::cout << "西施的年龄是:" << gl["西施"] << "\n";
}
运行这段代码,输出为:
西施的年龄是:18
那么有个问题:如果我要用下标访问不在map中的元素呢?是不是会失败?我们也试试,就接着上面的代码写:
std::cout << "杨玉环的年龄是:" << gl["杨玉环"] << "\n";
发现程序没有报错!居然能正常运行!:
杨玉环的年龄是:0
这就是map的特殊之处了:在map中使用随机访问,传入想要查找的键,如果这个键存在,则返回它相对应的值;若是不存在,则创建一个新的键值对,其对应的值进行值初始化。
根据这个特点,在执行了gl[“杨玉环”]之后,我们理应是能在map中找到它的:
std::map<std::string, int>::iterator temp = gl.find("杨玉环");
std::cout << temp->first << " " << temp->second << "\n";
运行之后发现确实如此。
(8) emplace()
先看看它的源码:
template< class... Args >
std::pair<iterator, bool> emplace( Args&&... args );
它的作用是:原位构造元素并插入map中。它的作用其实和之前说的insert()一样,但是它比insert()更高效:emplace()是直接构造元素,而insert需要构造std::pair类型的临时变量后再进行插入操作。
使用insert()添加元素:
#include<iostream>
#include<map>
int main() {
std::map<std::string, int> opt;
//方法一:显示构造
opt.insert(std::make_pair<std::string, int>("西施", 18));
//方法二:隐式转换构造
opt.insert(std::make_pair("貂蝉", 19));
for (std::pair<std::string, int> temp : opt) {
std::cout << "name = " << temp.first << ",age = " << temp.second << '\n';
}
}
使用emplace()添加元素:
#include<iostream>
#include<map>
int main() {
std::map<std::string, int> opt;
opt.emplace("西施", 18);
opt.emplace("貂蝉", 19);
for (std::pair<std::string, int> temp : opt) {
std::cout << "name = " << temp.first << ",age = " << temp.second << '\n';
}
}
这两段代码的效果是完全一样的,只是使用emplace()更加的便捷、高效。
结语
至此,普通map就讲完了,但是map不止有这一种,它的键值不能重复,因此还有键值能重复的map——multimap。
如果觉得我写的不错,可以点赞收藏给予我鼓励,若是有出入或者不足,也请在评论区指出,我会立即修改!