C++_map_set详解

关联容器的基本介绍

关联容器支持高效的关键字查找和访问。map和set是最主要关联容器。关联容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。C++标准库中提供了8个关联容器:

|--------------------|-------------------|
| 关联容器的类型 ||
| 按关键字有序保存元素 ||
| map | 关联数组;保存键值对 |
| set | 关键字即值,即只保存关键字的容器 |
| multimap | 关键字可重复出现的map |
| multiset | 关键字可重复出现的set |
| 无序集合 ||
| unordered_map | 用哈希函数组织的map |
| unordered_set | 用哈希函数组织的set |
| unordered_multimap | 哈希组织的map;关键字可重复出现 |
| unordered_multiset | 哈希组织的set;关键字可重复出现 |

其中,类型map和multimap定义在头文件map中;set和multiset定义在头文件set中;无序容器则定义在头文件unordered_map和unordered_set中。本文重点讨论map与set

键值对

用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key相对应的信息。比如 可以将一个人的名字作为关键字key,将其电话号码作为值value,这样我们就可以通过人名来找到对应的电话号码。

pair类型

pair是一个标准库类型,它定义在头文件utility中。pair文档

一个pair保存两个数据成员。类似容器,pair是一个用来生成特定类型的模板,下面是pair的原型

cpp 复制代码
// SGI-STL中关于键值对的定义
template <class T1, class T2>
struct pair
{
    typedef T1 first_type;
    typedef T2 second_type;
    
    pair()                            // 构造
        : first(T1())
        , second(T2())
    {}
    pair(const T1& a, const T2& b)    // 拷贝构造
        : first(a)
        , second(b)
    {}

    T1 first;                         // 成员变量
    T2 second;
};

当创建一个pair时,我们必须提供两个类型名,pair的数据成员将具有对应的类型

cpp 复制代码
// 代码示例
pair<string, string> a;         // 保存两个string
pair<string, int> b;            // 保存一个string和一个int
pair<string, vector<int>> c;    // 保存一个string和vector<int>

pair的默认构造函数会对数据成员进行值初始化。因此a是一个包含两个空string的pair;b中的int成员值初始化为0,而string成员初始化为空;c保存了一个空string和一个空的vector。

我们也可以为每个成员提供初始化器:

cpp 复制代码
// 创建一个名为object的pair,两个成员分别被初始化为"xxxx"和2
pair<string, int> object {"xxxx", 2};

|------------------------------|-----------------------------------------------|
| pair的常见操作 ||
| pair<T1, T2> p; | p是一个pair,两类型分别为T1和T2的成员进行了 值初始化 |
| pair<T1, T2> p (v1, v2); | p是一个成员类型为T1和T2的pair;first和second成员分别用v1,v2初始化 |
| pair<T1, T2> p = {v1, v2}; | 等价于 p (v1, v2) |
| make_pair(v1, v2); | 返回一个用v1和v2初始化的pair。pair的类型从v1和v2的类型推断出来 |

set

set的基本介绍

  1. set是按照一定次序存储元素的容器
  2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
  3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格的排序准则(默认是小于)进行排序。
  4. set在底层是用二叉搜索树(红黑树)实现的。

set的基本使用

set的构造

|--------------------------------------------------------------------------------------------------------------------|----------------------------|
| 函数声明 | 功能介绍 |
| set (const Compare& comp = Compare(), const Allocator& = Allocator() ); | 构造空的set |
| set (InputIterator first, InputIterator last, const Compare& comp = Compare(), const Allocator& = Allocator() ); | 用[first, last)区间中的元素构造set |
| set ( const set<Key,Compare,Allocator>& x); | set的拷贝构造 |

cpp 复制代码
// set构造
std::set<int> first;                                  // empty set of ints

int myints[]= {10,20,30,40,50};
std::set<int> second (myints,myints+5);               // range
std::set<int> third (second);                         // a copy of second
std::set<int> fourth (second.begin(), second.end());  // iterator ctor.

set的迭代器

虽然set同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素,因为set中的关键字是const的。可以用一个set迭代器来读取元素的值,但不能修改。

cpp 复制代码
set<int> s = {0,1,2,3,4,5,6,7,8,9};
set<int>::iterator it = s.begin();
if(it != s.end())
{
    //*it = 20;              // 错误,set中的关键字是只读的
    cout << *it << endl;     // 正确,可以读关键字
}

set的修改操作

|------------------------------------------------------|------------------------------------------------------------------------------------------------------------|
| 函数声明 | 功能介绍 |
| pair<iterator,bool> insert (const value_type& k ) | 在set中插入元素k,实际插入的是<k, k>构成的 键值对,如果插入成功,返回<该元素在set中的 位置,true>,如果插入失败,说明k在set中已经 存在,返回<k在set中的位置,false> |
| void erase ( iterator position ) | 删除set中position位置上的元素 |
| void erase ( iterator first, iterator last ) | 删除set中[first, last)区间中的元素 |
| size_type erase ( const key_type& k ) | 删除set中值为k的元素,返回删除的元素的个数 |

插入操作

insert操作向容器中添加一个元素或一个元素范围。

cpp 复制代码
vector<int> v = {2,4,6,8,2,4,6,8};
set<int> s;
s.insert(v.begin(), v.end());        // 迭代器
s.insert({1,3,5,7,1,3,5,7});         // 初始化器列表

insert有两个版本,分别接受一对迭代器,或是一个初始化器列表,这两个版本的行为类似对应的构造函数。

删除操作

set定义了三个版本的erase,如上表所示。与vector(序列式容器)一样,我们可以通过传递给erase一个迭代器 或者 一个元素范围。这两个版本的erase与对应的序列式容器的操作非常相似:指定元素被删除,函数返回void。

set提供了一个额外的erase操作,此版本删除所有匹配给定关键字的元素(如果存在的话),返回实际删除的元素的数量。就set来说此版本的erase的返回值总是0或者1。若返回0则说明该元素不存在,返回1则说明存在;但是如果在multiset上面,返回值可能会大于1。

set的访问操作

|----------------------------------------------|------------------------------------------------|
| 函数说明 | 功能简介 |
| iterator find ( const key_type& k ) const | 返回一个迭代器,指向第一个关键字为 k 的元素, 若k不在容器中,则返回尾后迭代器end() |
| size_type count ( const key_type& k ) const | 返回关键字等于 k 的元素的数量。 |

cpp 复制代码
set<int> s = {0,1,2,3,4,5,6,7,8,9};
s.find(1);            // 返回一个迭代器,指向key == 1的元素
s.find(20);           // 返回一个迭代器,其值等于s.end()
s.count(1);           // 返回1
s.count(20);          // 返回0

map

map的基本介绍

  1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
  2. 在map中,键值key通常用于排序和唯一地标识元素,而值value中存储与此键值key关的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair:typedef pair<const key, T> value_type;
  3. 在内部,map中的元素总是按照键值key进行比较排序的。
  4. map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
  5. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。

map的基本使用

map的构造

|----------------------|-----------|
| 函数声明 | 功能介绍 |
| map() | 构造一个空的map |
| map (const map& x); | 拷贝构造 |

map的迭代器

注意:map的value_type是一个pair类型,其first成员保存const的关键字,second成员保存值。我们可以改变pair的值,但是不能改变关键字成员的值!!!

cpp 复制代码
map<string, string> m;
// 使用insert插入元素到m
m.insert(pair<string, string>("peach", "桃子"));
m.insert(make_pair("banan", "香蕉"));

map<string, string>::iterator it = m.begin();
// 遍历map
while(it != m.end())
{
    cout << it->first << " " << it->second << endl;// 打印此元素的关键字和值
    //it->first = "apple"                          // 错误,关键字是const
    it->second++;                                  // 正确,可以通过迭代器改变值
}

map的修改操作

|------------------------------------------------------|--------------------------------------------------------------------|
| 函数声明 | 功能简介 |
| pair<iterator,bool> insert (const value_type& k ) | 在map中插入键值对k,注意 k 是一个键值 对,返回值也是键值对:iterator代表新插入 元素的位置,bool代表释放插入成功 |
| void erase ( iterator position ) | 删除map中position位置上的元素 |
| void erase ( iterator first, iterator last ) | 删除map中[first, last)区间中的元素 |
| size_type erase ( const key_type& k ) | 删除map中值为k的元素,返回删除的元素的个数 |

插入操作

对map进行insert操作时,必须要记住元素类型是一个pair。通常,对于想要插入的数据,并没有一个现成的pair对象。但是可以在insert的参数列表中创建一个pair。

cpp 复制代码
// pair的构造方式
map<string, string> dict;
// 方式一:有名对象
pair<string, string> kv("left", "左边");
dict.insert(kv);
// 方式二:匿名对象
dict.insert(pair < string, string>("right", "右边"));
// 方式三:make_pair函数模板(make_pair帮你构造)
dict.insert(make_pair("up", "上边"));
// 方式四:多参数构造函数-隐式类型转换--中间生成临时对象
pair<string, string> kv1 = { "down", "下边" };
dict.insert({ "down", "下边" });
// 方式五:initializer_list构造
map<string, string> dict = { {"left", "左边"}, {"right", "右边"}, {"up", "上边"} ,{"down", "下边"} };
删除操作

参考set的删除操作

map的访问操作

|-------------------------------------------------|--------------------------------------------------------------------------------|
| 函数声明 | 功能简介 |
| iterator find ( const key_type& k ) | 返回一个迭代器,指向第一个关键字为 k 的元素, 若k不在容器中,则返回尾后迭代器end() |
| size_type count ( const key_type& k ) const | 返回key为k的键值在map中的个数,注意map中key 是唯一的,所以该函数的返回值要么为0,要么为1, 因此也可以用该函数来检测一个key是否在map中 |
| mapped_type& operator[] (const key_type& k) | 返回key对应的value |

ps. 其中find函数与count函数请参见set

再谈insert

前面我们已经了解过insert函数作用和使用场景,下面我们再来谈谈insert的返回值。

仔细观察insert的函数声明不难发现,其返回值是一个pair。其中pair的first成员是一个迭代器,指向指定关键字的元素;second成员是一个bool值,指出元素是插入成功还是已经存在容器中。如果关键字已在容器中,则insert什么也不做,且其中的bool值为false;如果关键字不存在,元素则被插入到容器中,且bool值为true。

cpp 复制代码
// 单词计数程序
map<string, int> m;
string word;
while(cin >> word)
{
    // 插入一个元素,关键字等于word, 值为1
    // 若word已经在m中,则insert什么也不做
    auto ret = m.insert({word, 1});
    if(!ret.second)                         // word已经在m中
    {
        ++((ret.first)->second);            // 递增计数器
    }
}

上面代码中的if语句检查返回值的bool部分,若为false,则说明插入操作并未发生。

明确了insert函数,下面我们再来谈谈map的下标操作operator[]。

operator[]

map和unordered_map容器提供了下标运算符,简单说,operator[]就是给我一个key,我返回key对应value的引用。operator[]文档

上面两张图非常详细的介绍了operator[]的基本原理,希望大家能够理解

cpp 复制代码
// 代码演示
map<string, int> m;
m["kevin"] = 1;
// 将会进行下面的操作
// 在m中搜索关键字为kevin的元素,未找到。
// 将一个新的键值对插入到m中,关键字是一个const string,保存kevin。值进行初始化为0
// 提取出新插入的元素,并赋值为1

对于map的下标操作,其行为与数组或vector上的下标操作很不相同:使用一个不在容器中的关键字作为下标,会添加一个具有此关键字的元素到map中

map的下标操作与其他下标运算符的另一个不同之处就是其返回类型。通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的。但map的下标操作会返回一个key对应value的引用;当解引用map的迭代器时,会得到一个pair对象。

与其他下标运算符相同的是,map的下标运算符返回一个左值。由于返回的是一个左值,所以我们可以读写元素。

cpp 复制代码
cout << m["kevin"];        // 用Kevin作为下标提取元素,会打印出1
++m["kevin"];              // 提取元素,将其增加1
cout << m["kevin"];        // 提取元素并打印,会打印出2
相关推荐
weixin_486681141 小时前
C++系列-STL容器中统计算法count, count_if
开发语言·c++·算法
基德爆肝c语言1 小时前
C++入门
开发语言·c++
怀九日1 小时前
C++(学习)2024.9.18
开发语言·c++·学习·面向对象·引用·
一道秘制的小菜1 小时前
C++第七节课 运算符重载
服务器·开发语言·c++·学习·算法
代码小狗Codog2 小时前
C++独立开发开源大数计算库 CBigNum
数据结构·c++
WenGyyyL2 小时前
力扣最热一百题——二叉树的直径
java·c++·算法·二叉树·深度优先
sdlkjaljafdg3 小时前
vector<bool>性能测试
开发语言·c++·算法
telllong3 小时前
使用llama.cpp 在推理MiniCPM-1.2B模型
c++·llama·llama.cpp
m0_631270406 小时前
标准C++(二)
开发语言·c++·算法
沫刃起6 小时前
Codeforces Round 972 (Div. 2) C. Lazy Narek
数据结构·c++·算法