序列式容器 与 关联式容器
我们知道:
- C++ 中,我们将 vector、list、queue 这种底层为线性序列的数据结构叫做 序列式容器,其存储的就是元素本身。
- 而 关联式容器 以键-值对的形式存储数据。每个键在容器中必须是唯一的,而值则与相应的键关联。
键-值对
即 <key,value>
结构,在C++标准库中,std::pair
是一个模板类,其存储的就是键值对。其定义如下:
namespace std {
template <class T1, class T2>
struct pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair();
pair(const T1& x, const T2& y);
template<class U, class V> pair(const pair<U, V>& p);
};
}
一般键是唯一且不可重复的标识符,用于查找和访问对应的值。值可以是任何数据类型,包括整数、字符串、对象等。
树形结构的关联式容器
C++ 中,标准库提供了 std::map 、std::set 、std::multimap、std::multimap
四个基于红黑树实现的关联式容器。下面依次介绍其性质与使用:
std::set
Cplusplus 官网set文档
性质
上面的cpp文档详细的介绍了set这个容器,下面我们对其性质进行总结概括:
-
有序性:set 中的元素按照其key值自动被排序。默认情况下使用元素的 < 操作符(小于)比较,也可以通过自定义比较函数来指定排序规则。插入 / 删除 新元素时,set 会自动确保元素的有序性,并且不会插入重复的元素。
-
唯一性:对于set,元素的值(value)即是键(key),而且每个value必须是唯一的。
-
不可修改性:set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
-
底层实现:通常基于红黑树实现,因此插入、删除和查找操作的平均时间复杂度都是 O(log n)。
-
迭代器支持:set 提供了正向迭代器,可以用于遍历集合中的元素。
Attention:
- set在创建时为
std::set<value> myset;
,只存放value,其底层上存放的是<value, value>
的键值对。实际实现中,只存储了键,而值部分为空。 - 我们知道set中的元素不可直接修改,为什么?
- 由于 std::set 是一种基于红黑树实现的关联容器,它要求元素在容器中保持有序性和唯一性。因此,直接修改元素可能会破坏红黑树的结构,导致容器不再符合要求。
使用
模板参数列表
下面是C++标准库中的 set 模板参数列表
template<
class Key, // Key(键)类型
class Compare = std::less<Key>, // 比较函数类型
class Allocator = std::allocator<Key> // 分配器类型
> class set;
分别解释:
- Key:
std::set
中存储的元素的类型。set 中的元素按照键值自动被排序,并且每个元素在中都是唯一的 - Compare:可选的比较函数类型,用于指定元素的比较规则。默认情况下,使用
<
操作符进行比较。 - 可选的分配器类型,用于分配内存和管理容器中的元素。
应用
数据去重:std::set
会自动对元素进行排序并移除重复的元素。
std::vector<int> numbers = {4, 1, 2, 2, 3, 4};
std::set<int> uniqueNumbers(numbers.begin(), numbers.end());
// 现在uniqueNumbers中包含的是去重后的元素{1, 2, 3, 4}
快速查找:std::set
内部基于红黑树实现,因此查找操作非常高效,时间复杂度为O(log n)
std::set<std::string> names = {"Alice", "Bob", "Charlie", "David"};
if (names.find("Bob") != names.end()) // 查找Bob
{
std::cout << "Bob 在set中" << std::endl;
}
迭代器的使用:可以通过迭代器来访问集合中的元素。
#include <iostream>
#include <set>
int main() {
std::set<int> mySet = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 使用迭代器遍历 set 中的所有元素
for (auto it = mySet.begin(); it != mySet.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// 反向迭代器
for (auto rit = mySet.rbegin(); rit != mySet.rend(); ++rit) {
std::cout << *rit << " ";
}
std::cout << std::endl;
// 使用迭代器查找特定元素
int target = 5;
auto findIt = mySet.find(target);
if (findIt != mySet.end()) {
std::cout << target << "找到了" << std::endl;
} else {
std::cout << target << "没找到" << std::endl;
}
return 0;
}
std::map
cplusplus 官网文档
性质
上面的cpp文档详细的介绍了map这个容器,下面我们对其性质进行总结概括:
-
有序性:map 中的元素按照key的大小顺序进行排序,默认情况下采用升序方式。这使得在 map 中进行范围遍历时,能够以键的顺序访问元素。
-
唯一键:map 中的键是唯一的,如果插入一个已经存在的键,则会覆盖原有的键对应的值。
-
查找效率:由于 map 基于红黑树实现,对于元素的查找、插入和删除操作具有较高的效率,时间复杂度为 O(log n)。
-
键-值对映射:map 提供了键和值之间的映射关系,通过键可以快速查找对应的值,这使得 map 适合用于需要快速查找和检索值的场景。
Attention:
- map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
模板参数列表
template <class Key, class T, class Compare = std::less<Key>, class Allocator = std::allocator<std::pair<const Key, T>>>
class map;
这里是对每个模板参数的解释:
-
Key:键的类型,用于唯一标识每个元素。
-
T:值的类型,与键相关联的数据类型。
-
Compare:可选参数,用于比较键的函数对象类型,默认情况下采用
std::less<Key>
,表示采用 < 操作符进行键的比较。 -
Allocator:可选参数,内存分配器的类型,默认情况下采用
std::allocator<std::pair<const Key, T>>
,表示使用标准分配器
应用
我们下面用map进行一些应用:字典、计数器。
字典
#include <iostream>
#include <map>
#include <string>
int main() {
// 创建字典
std::map<std::string, std::string> dictionary;
// 添加词条到字典
dictionary["apple"] = "苹果";
dictionary["banana"] = "香蕉";
dictionary["cat"] = "猫";
// 查找词条
std::string word = "";
getline(word);
if (dictionary.find(word) != dictionary.end()) {
std::cout << word << ": " << dictionary[word] << std::endl;
} else {
std::cout << "字典中未查到该词" << std::endl;
}
return 0;
}
上面的代码中,我们使用map<string, string>
来模拟字典,key,value分别对应一个单词的英文和中文含义。
计数器
#include <iostream>
#include <map>
int main() {
std::map<int, int> counter;
// 统计数字出现的次数
int numbers[] = {5, 3, 7, 5, 3, 9, 5};
for (int num : numbers) {
counter[num]++;
}
// 输出数字及其出现的次数
for (const auto& pair : counter) {
std::cout << pair.first << " occurs " << pair.second << " times" << std::endl;
}
return 0;
}
计数器通过将value值设为int类型,可以统计值为key的元素的出现个数。
std::multiset
cpluscplus官网文档
性质
multiset 的 模板参数列表 与 set 是相同的,但在性质上有所差别。
这里我们先看 多重集(Multiset)和集合(Set)之间 的区别:
-
元素重复性:multiset 允许元素重复出现,而 set 中每个元素都是唯一的。
-
插入操作:在 set 中,插入已经存在的元素会被忽略,因为元素不能重复;而在 multiset 中,可以插入重复的元素,每个元素将会被单独计数。
-
查找操作:在 set 中,由于元素唯一,查找特定元素的速度较快;而在 multiset 中,由于元素可能重复,查找特定元素需要遍历多个相同元素。
-
应用场景:set 适合用于需要保持元素唯一性的场景,比如存储不重复的关键字;而 multiset 则适用于需要统计元素重复次数的场景,比如统计数据集中各个元素的出现频率。
使用
当有以下情况时,我们会使用 multiset 而不是set:
- 需要存储重复元素的情况
- 不需要维护元素的唯一性
- 需要快速查找和删除重复元素
下面的代码展示了,multiset的使用:
int main() {
std::multiset<int> numbers;
// 插入元素
numbers.insert(10);
numbers.insert(20);
numbers.insert(30);
numbers.insert(20); // 插入重复的元素
// 输出元素
std::cout << "该multiset包含: ";
for (const auto& num : numbers) {
std::cout << " " << num;
}
std::cout << std::endl;
// 统计特定元素的重复次数
int target = 20;
int count = numbers.count(target);
printf("值为%d的元素共有%d个\n", target, count);
// 删除指定元素
numbers.erase(target);
// 再次输出元素
printf("删除元素%d后,multiset共包含:", target);
for (const auto& num : numbers) {
std::cout << " " << num;
}
std::cout << std::endl;
return 0;
}
代码输出结果:
std::multimap
cpluscplus官网文档
性质
这里我们主要关心 multimap 与 map 的区别(性质 及 操作):
-
键的唯一性: map中的键是唯一的,每个键只能对应一个值。而multimap允许多个键有相同的值,一个键可以对应多个值。
-
插入顺序: map按照键的大小顺序进行排序,并保持该顺序。multimap不对键进行排序,插入的顺序被保留。
-
查找操作: 在map中,由于键的唯一性,使用find()函数查找一个键时,如果找到了就返回该键对应的值,如果没找到则返回end()迭代器。而在multimap中,find()函数返回指向第一个匹配的键-值对的迭代器,如果没有匹配的键,则返回end()迭代器。
-
删除操作: 在map中,使用erase()函数删除一个键时,如果该键存在,则删除该键对应的键-值对,并将其从容器中移除。而在multimap中,erase()函数会删除所有与给定键匹配的键-值对。
总的来说,multimap 提供了一种允许重复键并自动排序的数据结构,适用于需要按照键进行检索且允许重复键的情况下使用。
应用
我们利用multimap 一个key可以对应多个value的特性,下面的示例使用 multimap 构建了一个学生表:
#include <iostream>
#include <map>
#include <string>
int main() {
std::multimap<std::string, std::string> studentCourses;
// 添加学生和他们的课程
studentCourses.insert(std::make_pair("Alice", "Math"));
studentCourses.insert(std::make_pair("Alice", "Physics"));
studentCourses.insert(std::make_pair("Bob", "Chemistry"));
studentCourses.insert(std::make_pair("Bob", "Biology"));
studentCourses.insert(std::make_pair("Bob", "Math"));
// 输出每个学生选修的课程
for (const auto& pair : studentCourses) {
std::cout << pair.first << " takes " << pair.second << std::endl;
}
return 0;
}
输出如下:
结尾
上面介绍的关联式容器,在做OJ题时也会经常用上,在理解其使用场景后做题,可以很好的运用它们。