目录
一、关联式容器概述
在STL中,容器分为两大类别:
-
序列式容器:线性数据结构(vector/list/deque等),存储元素本身
-
关联式容器:存储<key,value>键值对,基于红黑树实现,具有高效检索特性
主要区别:
-
存储内容:键值对 vs 独立元素
-
检索效率:O(logN) vs O(N)
-
元素顺序:自动排序 vs 插入顺序
二、键值对
键值对是用来表示具有一一对应关系的一种结构,一般包含两个成员变量 key 和 value,key 代表键值,value 表示与 key 对应的信息。例如建立英汉互译字典,英文单词与其中文含义是一一对应的。
在 SGI-STL 中,键值对的定义如下:
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) {}
};
三、树形结构的关联式容器
STL 中实现了两种不同结构的关联式容器:树型结构与哈希结构。树型结构的关联式容器主要有四种:map、set、multimap、multiset,它们都使用平衡搜索树(红黑树)作为底层结构,容器中的元素是有序的序列。
(一)set
1. set的介绍
翻译:
- set 是按照一定次序存储元素的容器。
- 在 set 中,元素的 value 也标识它(value就是 key,类型为 T),并且每个 value 必须唯一的。set 中的元素不能在容器中修改(元素总是const),但可以从容器中插入或删除它们。
- 在内部,set 中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
- set 容器通过key 访问单个元素的速度通常比 unordered_set容器慢,但它们允许按顺序对子集进行直接迭代。
- set 在底层是用二叉搜索树(红黑树)实现的。
注意:
- 与 map/multimap 不同,map/multimap 存储的是真正的键值对<key, value>,set 中只放 value,但在底层实际存放的是由<value, value>构成的键值对。
- set 中插入元素时,只需插入 value 即可,无需构造键值对。
- set 中的元素不可重复,(因此可以使用set进行去重)。
- 使用set的迭代器遍历set中的元素,可以得到有序序列。
- set中的元素默认按照小于来比较。
- set 中查找某个元素的时间复杂度为 O(log₂n)。
- set 中的元素不允许修改,因为其底层是二叉搜索树(红黑树)。
2. set的使用
(1)模板参数列表
参数名称 | 说明 |
---|---|
T | set 中存放元素的类型,底层存储 <value, value> 的键值对。 |
Compare | 比较函数对象的类型,默认根据 less<T> 按元素是否小于另一元素排序。 |
Alloc | 用于元素分配和管理空间的分配器,默认使用 STL 提供的空间配置器。 |
(2)构造函数
构造函数 | 说明 |
---|---|
set (const Compare& comp = Compare(), const Allocator& alloc = Allocator()) | 构造空的 set ,comp 用于比较,alloc 用于空间管理。 |
set (InputIterator first, InputIterator last, const Compare& comp = Compare(), const Allocator& alloc = Allocator()) | 使用范围 [first, last) 中的元素构造 set ,comp 和 alloc 同上。 |
set (const set<Key, Compare, Allocator>& x) | 拷贝构造,用 x 的内容初始化新 set 。 |
(3)迭代器函数
迭代器函数 | 说明 |
---|---|
begin() | 返回指向起始位置元素的迭代器。 |
end() | 返回指向最后一个元素后面位置的迭代器。 |
cbegin() | 返回指向起始位置元素的 const 迭代器。 |
cend() | 返回指向最后一个元素后面位置的 const 迭代器。 |
rbegin() | 返回指向第一个元素的反向迭代器,即指向末尾的迭代器。 |
rend() | 返回指向最后一个元素下一个位置的反向迭代器,即指向起始位置的迭代器。 |
crbegin() | 返回指向第一个元素的反向 const 迭代器,即指向 cend() 。 |
crend() | 返回指向最后一个元素下一个位置的反向 const 迭代器,即指向 cbegin() 。 |
(4)容量函数
容量函数 | 说明 |
---|---|
empty() | 检测 set 是否为空,若为空返回 true ,否则返回 false 。 |
size() | 返回 set 中有效元素的个数。 |
(5)修改操作函数
修改操作函数 | 说明 |
---|---|
insert(const value_type& x) | 插入元素 x ,实际插入的是 <x, x> 构成的键值对。若插入成功,返回包含该元素在 set 中位置和 true 的 pair ;若失败(x 已存在),返回包含 x 在 set 中位置和 false 的 pair 。 |
erase(iterator position) | 删除 position 位置上的元素。 |
erase(const key_type& x) | 删除值为 x 的元素,返回删除的元素个数。 |
erase(iterator first, iterator last) | 删除 [first, last) 区间中的元素。 |
swap(set<Key, Compare, Allocator>& st) | 交换两个 set 中的元素。 |
clear() | 清空 set 中的元素。 |
find(const key_type& x) | 返回值为 x 的元素的位置。 |
count(const key_type& x) | 返回值为 x 的元素的个数。 |
(6)常用接口
// 构造
set<int> s1; // 空set
set<int> s2(arr, arr+arrSize); // 数组构造
// 插入删除
s.insert(10); // 插入元素
s.erase(20); // 删除元素
// 查找
auto pos = s.find(5); // 返回迭代器
int cnt = s.count(5); // 存在返回1,否则0
// 容量
bool isEmpty = s.empty();
size_t size = s.size();
(7)使用举例
#include <iostream>
#include <set>
using namespace std;
int main()
{
//set<int> s;
//s.insert(5);
//s.insert(3);
//......
int arr[] = { 5,3,7,1,9,2,5,3 }; // 包含重复元素
set<int> s(arr, arr + sizeof(arr) / sizeof(int));
cout << "Set size: " << s.size() << endl; // 输出6(自动去重)
// 正向遍历(自动排序)
cout << "Ascending: ";
for (auto& num : s)
{
cout << num << " "; // 1 2 3 5 7 9
}
// 反向遍历
cout << "\nDescending: ";
for (auto it = s.rbegin(); it != s.rend(); ++it)
{
cout << *it << " "; // 9 7 5 3 2 1
}
// 查找测试
cout << "\nCount of 3: " << s.count(3) << endl; // 1
cout << "Count of 4: " << s.count(4) << endl; // 0
}
(二)map
1. map的介绍
翻译:
- map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
- 在map中,键值key通常用于排序和唯一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair:
- typedef pair<const key, T> value_type;
- 在内部,map中的元素总是按照键值key进行比较排序的。
- map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
- map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
- map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。
注意:
- map 中的元素是键值对。
- key 是唯一的,且不能修改。
- 默认按小于的方式对 key 进行比较。
- 遍历时可得到按 key 排序的序列。
- 底层为平衡搜索树(红黑树),查找效率高,时间复杂度为 O(log₂N)。
- 支持 [ ] 操作符,operator[ ] 中实际进行插入查找。
2. map的使用
(1)模板参数说明
参数名称 | 说明 |
---|---|
Key | 键值对中 key 的类型。 |
T | 键值对中 value 的类型。 |
Compare | 比较器的类型,默认按小于比较,对于内置类型元素一般无需传递,自定义类型时需显式传递比较规则(一般用函数指针或仿函数)。 |
Alloc | 通过空间配置器申请底层空间,默认使用标准库提供的空间配置器,除非用户另有指定。 |
(2)构造函数
构造函数 | 说明 |
---|---|
map() | 构造一个空的 map 。 |
(3)迭代器函数
迭代器函数 | 说明 |
---|---|
begin() | 返回指向首元素的迭代器。 |
end() | 返回指向最后一个元素的下一个位置的迭代器。 |
cbegin() | 返回指向首元素的 const 迭代器,指向的元素不能修改。 |
cend() | 返回指向最后一个元素的下一个位置的 const 迭代器,指向的元素不能修改。 |
rbegin() | 返回反向迭代器,初始指向最后一个元素(即 end 位置),其 ++ 操作向首元素方向移动。 |
rend() | 返回反向迭代器,初始指向第一个元素的前面(即 begin 位置),其 -- 操作向尾元素方向移动。 |
crbegin() | 返回反向 const 迭代器,功能同 rbegin() ,但指向的元素不能修改。 |
crend() | 返回反向 const 迭代器,功能同 rend() ,但指向的元素不能修改。 |
(4)容量与元素访问函数
容量与元素访问函数 | 说明 |
---|---|
empty() | 检测 map 是否为空,若为空返回 true ,否则返回 false 。 |
size() | 返回 map 中有效元素的个数。 |
operator[] | 通过 key 获取对应的 value。当 key 不存在时,会用默认 value 构造键值对并插入,然后返回该默认 value。 |
at() | 与 operator[] 类似,但当 key 不存在时直接抛异常。 |
(5)元素修改函数
元素修改函数 | 说明 |
---|---|
insert(const value_type& x) | 插入键值对 x ,返回值包含新插入元素的位置和是否插入成功。 |
erase(iterator position) | 删除 position 位置上的元素。 |
erase(const key_type& x) | 删除键值为 x 的元素。 |
erase(iterator first, iterator last) | 删除 [first, last) 区间中的元素。 |
swap(map<Key, T, Compare, Allocator>& mp) | 交换两个 map 中的元素。 |
clear() | 清空 map 中的元素。 |
find(const key_type& x) | 查找键值为 x 的元素,找到返回该元素的位置的迭代器,否则返回 end 。 |
count(const key_type& x) | 返回键值为 x 的键值在 map 中的个数,由于 map 中 key 唯一,返回值要么为 0,要么为 1。 |
(6)常用接口
map<string, int> m;
// 插入
m.insert(make_pair("apple", 5));
m["banana"] = 3; // 使用[]插入
// 访问
int num = m["apple"]; // 不存在会插入默认值
int num2 = m.at("apple"); // 不存在抛异常
// 删除
m.erase("banana");
// 查找
auto pos = m.find("apple");
if(pos != m.end()) {
cout << pos->first << ": " << pos->second;
}
(7)使用示例
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main()
{
map<string, string> m;
// 向map中插入元素的几种方式
pair<string, string> kv("peach", "桃子");
m.insert(kv); // 有名对象
m.insert(pair<string, string>("peach", "桃子")); // 匿名对象
m.insert(make_pair("banana", "香蕉")); // 函数模板
m.insert({ "apple", "苹果" }); // 隐式类型转换
m["apple"] = "苹果"; // 若key不存在则插入,默认构造value然后赋值
// 用迭代器遍历map,可得到按key排序的序列
cout << "Map contents:" << endl;
for (auto& e : m)
{
cout << e.first << " ---> " << e.second << endl;
}
// 尝试插入已存在的key
auto ret = m.insert(make_pair("peach", "桃色"));
if (!ret.second)
{
cout << "Key 'peach' already exists: " << ret.first->first << " ---> "
<< ret.first->second << " Insert failed" << endl;
}
// 删除key为"apple"的元素
m.erase("apple");
if (m.count("apple") == 0)
{
cout << "Apple has been erased" << endl;
}
// 修改值
m["banana"] = "大香蕉"; // 修改已有项
// 遍历输出
cout << "Map after update:" << endl;
for (auto& entry : m)
{
cout << entry.first << " => " << entry.second << endl;
}
// 危险操作:访问不存在的键
cout << "Peach: " << m["peach"] << endl; // 插入空值
// m.at("watermelon") = "西瓜"; // 抛出异常
// 安全查找
auto pos = m.find("pear");
if (pos != m.end())
{
cout << "Found pear: " << pos->second << endl;
}
else
{
cout << "Pear not found!" << endl;
}
}
(三)multiset
1. multiset的介绍
翻译:
- multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
- 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是<value, value>组成的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器中进行修改(因为元素总是const的),但可以从容器中插入或删除。
- 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则进行排序。
- multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭代器遍历时会得到一个有序序列。
- multiset底层结构为二叉搜索树(红黑树)。
注意:
- multiset在底层中存储的是<value, value>的键值对
- mtltiset的插入接口中只需要插入即可
- 与set的区别是,multiset中的元素可以重复,set是中value是唯一的
- 使用迭代器对multiset中的元素进行遍历,可以得到有序的序列
- multiset中的元素不能修改
- 在multiset中找某个元素,时间复杂度为O(log₂N)
- multiset的作用:可以对元素进行排序
2. multiset的使用
#include <set>
#include <iostream>
using namespace std;
int main()
{
int array[] = { 2, 1, 3, 3, 9, 6, 6, 0, 5, 8, 4, 7 };
multiset<int> s(array, array + sizeof(array) / sizeof(array[0]));
// 遍历multiset,可得到按升序排序且允许重复元素的序列
for (auto& e : s)
cout << e << " ";
cout << endl;
}
(四)multimap
1. multimap的介绍
翻译:
- Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对<key,value>,其中多个键值对之间的key是可以重复的。
- 在multimap中,通常按照key排序和唯一地标识元素,而映射的value存储与key关联的内容。key和value的类型可能不同,通过multimap内部的成员类型value_type组合在一起,value_type是组合key和value的键值对:
- typedef pair<const Key, T> value_type;
- 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对key进行排序的。
- multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代器直接遍历multimap中的元素可以得到关于key有序的序列。
- multimap在底层用二叉搜索树(红黑树)来实现。
注意:
- map中的key是唯一的,而multimap中key是可以重复的。
- 不支持 [ ] 运算符。
2. multimap的使用
#include <map>
#include <iostream>
using namespace std;
int main()
{
multimap<string, string> contacts;
contacts.insert(make_pair("John", "123-4567"));
contacts.insert(make_pair("John", "123-4568"));
contacts.insert(make_pair("Alice", "555-1234"));
// 查找所有John的联系方式
auto range = contacts.equal_range("John");
cout << "John's numbers:\n";
for (auto it = range.first; it != range.second; ++it)
{
cout << it->second << endl;
}
}
四、容器对比
特性 | set | multiset | map | multimap |
---|---|---|---|---|
存储元素 | 唯一值 | 可重复值 | 唯一键 | 可重复键 |
数据组织 | 值即键 | 值即键 | 键值对 | 键值对 |
[ ]运算符 | 不支持 | 不支持 | 支持 | 不支持 |
典型应用场景 | 去重/排序 | 排序统计 | 字典/映射 | 多映射 |