STL之关联容器(map ,set)
1.概述
在 C++ 的标准模板库(STL)的丰富工具集中,set
和map
作为关联容器,扮演着极为关键的角色,为开发者处理复杂数据场景提供了有力支持。它们拥有一些显著的共性,皆基于红黑树这种自平衡二叉搜索树的数据结构构建,这使得二者在元素的插入、删除和查找操作上,平均时间复杂度均能达到令人满意的 O (log n),高效地满足了各类程序对数据快速处理的需求。不过,set
和map
又在诸多方面展现出鲜明的个性。set
专注于构建一个包含唯一元素的集合,主要目的在于高效地判断某个元素是否存在于集合之中;而map
则致力于维护键值对的映射关系,能根据特定的键快速检索到与之关联的值。接下来,就让我们全方位、深层次地对比剖析set
和map
在存储结构、操作方法以及适用场景等维度的异同之处,助力你透彻理解并灵活运用这两种强大的容器,大幅提升 C++ 程序开发的效率与质量 。
2.数据结构
黑红树
红黑树本质上是一种特殊的二叉搜索树,它在每个节点上增加了一个存储位来表示节点的颜色(红色或黑色)。通过对任何一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
不过多介绍 它就是一个有序 并且查找删除效率高的 而且是有序的
有序性的体现
-
节点值的有序排列:在红黑树中,对于任意一个节点,其左子树中所有节点的值都小于该节点的值,而其右子树中所有节点的值都大于该节点的值。这种特性保证了红黑树中的节点是按照节点值的大小有序排列的。
-
中序遍历有序输出:对红黑树进行中序遍历(即先访问左子树,再访问根节点,最后访问右子树),可以得到一个按节点值从小到大排列的有序序列。例如,在一个存储整数的红黑树中,通过中序遍历可以依次输出从小到大排列的整数。
map
他把中间的值换成了key-val
set
就是红黑树 只不过不能有重复的值 而且可以是任意string以及vector这些
3.操作方法
创建
std::set<Key> s; // 创建一个空的 set,Key 是元素类型
std::map<key,val>m;// 创建一个空的 map,Key 是键的类型,T 是值的类型
//区间范围
std::set<key>s(InputIterator first, InputIterator last)
std::map<Key, T> m(InputIterator first, InputIterator last);
std::set<Key> s1(s2);
std::map<Key, T> m1(m2);
std::set<Key> s(std::move(s2));
std::map<Key, T> m(std::move(m2));
解释一下 m的区间范围 ,你得保证要么就是一个map,要么pair<>类型
例子:
#include <iostream>
#include <map>
#include <vector>
#include <utility>
int main() {
// 创建一个包含键值对的 vector
std::vector<std::pair<int, std::string>> vec = {
{1, "apple"},
{2, "banana"},
{3, "cherry"}
};
// 使用范围构造函数从 vector 初始化 map
std::map<int, std::string> myMap(vec.begin(), vec.end());
// 遍历 map 并输出元素
for (const auto& pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
访问
属性
size_type size() const; // 返回 set和map 中元素的数量
bool empty() const; // 判断 set和 map是否为空
iterator find(const Key& k);
// 在 set 中查找键为 k 的元素或者在map中寻找k-v,如果找到返回指向该元素的迭代器,否则返回 end()
元素之set
#include <iostream>
#include <set>
int main() {
std::set<int> mySet = {3, 1, 2};
// 使用范围 for 循环(底层也是迭代器)
for (const auto& element : mySet) {
std::cout << element << " ";
}
std::cout << std::endl;
// 使用普通迭代器
for (auto it = mySet.begin(); it != mySet.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
元素之map
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap = {{1, "one"}, {2, "two"}, {3, "three"}};
// 使用范围 for 循环
for (const auto& pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
// 使用普通迭代器
for (auto it = myMap.begin(); it != myMap.end(); ++it) {
std::cout << it->first << ": " << it->second << std::endl;
}
return 0;
}
这里,pair.first
表示键,pair.second
表示值;对于迭代器 it
,可以使用 it->first
和 it->second
来分别访问键和值。
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> myMap = {{1, "one"}, {2, "two"}, {3, "three"}};
// 使用 [] 运算符
std::cout << myMap[2] << std::endl;
}
可以使用 []
运算符
增加
std::pair<iterator, bool> insert(const value_type& val);
// 插入范围
template <class InputIterator>
void insert(InputIterator first, InputIterator last)
template <class... Args>
std::pair<iterator, bool> emplace(Args&&... args);
// 直接在 set和map 中构造元素
//map 使用 [] 运算符插入键值对
myMap[6] = "six";
std::pair<iterator, bool>
- 第一个成员类型为容器的迭代器(iterator),它指向插入的元素或者指向已经存在的具有相同键的元素
- 第二个成员类型为 bool,表示插入操作是否成功。如果插入的元素是新元素(即容器中原本不存在具有相同键的元素),则返回 true;如果容器中已经存在具有相同键的元素,插入操作不会重复插入该元素,此时返回 false。
关于直接构造
insert
插入方式当使用
insert
函数插入元素时,通常需要先创建一个完整的对象,然后将这个对象传递给insert
函数。这可能涉及到对象的构造、拷贝或移动操作。
emplace
函数则允许你直接在容器内部构造对象,它接受的是用于构造对象的参数,而不是一个已经构造好的对象
例子:
#include <iostream>
#include <map>
#include <string>
class ExpensiveToCopy {
public:
ExpensiveToCopy(int value, const std::string& str) : data(value), text(str) {
std::cout << "Constructor called" << std::endl;
}
ExpensiveToCopy(const ExpensiveToCopy& other) : data(other.data), text(other.text) {
std::cout << "Copy constructor called" << std::endl;
}
ExpensiveToCopy(ExpensiveToCopy&& other) noexcept : data(other.data), text(std::move(other.text)) {
std::cout << "Move constructor called" << std::endl;
}
int data;
std::string text;
};
int main() {
std::map<int, ExpensiveToCopy> myMap;
std::cout << "Using insert:" << std::endl;
// 使用 insert 插入元素
myMap.insert({1, ExpensiveToCopy(10, "hello")});
std::cout << "\nUsing emplace:" << std::endl;
// 使用 emplace 插入元素
myMap.emplace(2, 20, "world");
return 0;
}
删除
// 删除指定位置的元素
iterator erase(iterator position);
// 删除指定键的元素
size_type erase(const Key& k);
// 删除指定范围的元素
iterator erase(iterator first, iterator last);
// 删除指定位置的键值对
iterator erase(iterator position);
// 删除指定键的键值对
size_type erase(const Key& k);
// 删除指定范围的键值对
iterator erase(iterator first, iterator last);
void clear(); // 清空 set 中的所有元素
4 应用场景
共性场景
- 构造开销大的对象插入:当存储的元素或键值对涉及自定义类型,且这些类型的构造、拷贝或移动操作开销较大时,
emplace
可直接在容器内部利用传入参数构造对象,避免额外临时对象的创建与复制,减少开销,显著提升性能。 - 频繁插入操作:在需要大量、频繁地向
std::set
或std::map
中插入元素的场景下,emplace
每次插入都能避免临时对象的创建和复制,随着插入次数增多,性能优势愈发明显。
各自特点场景
std::set
- 适用于存储自定义类型元素,通过
emplace
可高效地将元素插入到集合中,利用其直接构造特性维护集合的有序性和元素唯一性。
std::map
- 复杂键值对存储:当键值对类型复杂,特别是值类型构造开销大时,
emplace
能直接在map
内部构造键值对,提高插入效率。 - 动态数据插入:在动态生成键值对并插入
map
的场景,如从文件或网络读取数据插入时,emplace
可更高效地处理操作,避免创建临时对象的开销。
5.multimap和multiset
相同点
都是黑红树
不同点:
元素
-
与
std::map
不同的是,std::multimap
允许存储多个具有相同键的键值对。 -
与
std::set
不同的是,std::multiset
允许存储重复的元素,即可以有多个元素具有相同的值
应用场景:
map:
- 一对多映射关系:当需要表示一对多的映射关系时,例如一个学生可以有多个成绩,一个作者可以有多本书等,
std::multimap
可以很好地满足需求。 - 按键分组数据:可以将具有相同键的数据分组存储在
std::multimap
中,方便后续的处理和查询。
set
统计元素出现次数:由于 std::multiset
允许存储重复元素,可以方便地统计某个元素在集合中出现的次数,使用 count()
函数即可实现。
维护有序序列且允许重复:当需要维护一个有序的元素序列,并且允许元素重复时,std::multiset
是一个不错的选择。