在之前的文章里,我们已经接触过STL中的部分容器,比如:vector、list、deque、forward_list(单链表)(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。那什么是关联式容器?它与序列式容器有什么区别?
关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
SGI-STL中关于键值对的定义:
template
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)
{}
};
这段代码是一个简单的模板类 pair,用于表示一对值的组合。
pair 类包含两个模板参数 T1 和 T2,分别表示两个值的类型。
该类定义了两个成员变量 first 和 second,分别表示第一个值和第二个值。
类中还定义了两个类型别名 first_type 和 second_type,用于方便访问第一个值和第二个值的类型。
该类提供了两个构造函数,一个是默认构造函数,会使用默认构造函数对 first 和 second 进行初始化;另一个是带参数的构造函数,可以接受两个值作为参数进行初始化。
这个模板类的作用是可以轻松地创建一对不同类型的值,并且可以通过 first 和 second 成员变量来访问和操作这些值。它可以在很多情况下提供便利,并且可以根据需要定义不同类型的 pair 实例。
根据应用场景的不桶,STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。树型结构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一个容器。
在C++中,set是标准库提供的一种容器,用于存储一组已排序的唯一元素。即set中的元素按照一定的排序规则进行排序,并且不允许重复。
set的特点包括:
使用set前需要包含头文件
注意:
1. 与map/multimap不同,map/multimap中存储的是真正的键值对
2. set中插入元素时,只需要插入value即可,不需要构造键值对。
3. set中的元素不可以重复(因此可以使用set进行去重)。
4. 使用set的迭代器遍历set中的元素,可以得到有序序列
5. set中的元素默认按照小于来比较
6. set中查找某个元素,时间复杂度为:logN
7. set中的元素不允许修改(为什么?)
因为在 C++ 的 set 容器中,元素的键值是唯一(伪键值对)的且不能被修改。这是因为 std::set 使用红黑树作为底层数据结构来实现有序性和快速查找的特性。
红黑树是一种自平衡的二叉搜索树,它的排序是基于节点的键值。当一个元素被插入到红黑树中时,它会被放置到正确的位置以保持树的有序性。如果允许修改元素的键值,那么它将改变元素在红黑树中的位置,破坏了树的有序性和平衡性。
为了维持红黑树的结构和性质,标准库规定在 std::set 中,元素的键值是只读的,一旦插入到容器中就不能被修改。如果确实需要修改键值,应该将该元素从 std::set 中移除,修改后再重新插入。
T: set中存放元素的类型,实际在底层存储
Compare:set中元素默认按照小于来比较
Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理
注:以下函数的参数类型可以配合函数模板一起看
函数声明 |
功能介绍 |
set (const Compare& comp = Compare(), const Allocator& |
构造空的set |
set (InputIterator first, InputIterator last, const Compare& comp = Compare(), const Allocator& = Allocator() ); |
用[first,last)区间中的元素构造set |
set ( const set |
set的拷贝构造 |
函数使用 |
功能介绍 |
iterator begin() |
返回set中起始位置元素的迭代器 |
iterator end() |
返回set中最后一个元素后面的迭代器 |
const_iterator cbegin() const |
返回set中起始位置元素的const迭代器 |
const_iterator cend() const |
返回set中最后一个元素后面的const迭代器 |
reverse_iterator rbegin() |
返回set第一个元素的反向迭代器,即end |
reverse_iterator rend() |
返回set最后一个元素下一个位置的反向迭代器,即rbegin |
const_rervrse_iterator crbegin() const |
返回set第一个元素的反向const迭代器,即cend |
const_reverse_iterator crend() const |
返回set最后一个元素下一个位置的反向const迭 |
iterator lower_bound (const value_type& val) const; |
返回迭代器的边界(>=),即第一个大于或等于给定键的元素的位置的迭代器。 |
iterator upper_bound (const value_type& val) const; |
返回迭代器的边界(>),即第一个严格大于给定键的元素的位置的迭代器。 |
pair |
返回一个区间 |
函数声明 |
功能介绍 |
bool empty() const |
检查set是否为空,返回true,否则返回false |
size_type size() const |
返回set中有效元素的个数 |
max_size |
函数声明 |
功能介绍 |
pair |
在set中插入元素x,实际插入的是 |
void erase ( iterator position ) |
删除set中position位置上的元素 |
size_type erase ( constkey_type& x ) |
删除set中值为x的元素,返回删除的元素的个数 |
void erase ( iterator first, iterator last ) |
删除set中[first, last)区间中的元素 |
void swap ( set |
交换set中的元素 |
void clear ( ) |
将set中的元素清空 |
terator find ( constkey_type& x ) const |
返回set中值为x的元素的位置 |
size_type count ( constkey_type& x ) const |
返回set中值为x的元素的个数 |
#include
#include
void test_set()
{
int arr[] = { 2,4,9,7,4,3,1,5,9,8,7,7 };
cout << sizeof(arr) / sizeof(arr) << endl;
//使用一段区间构造set
//set的作用排序+去重
set mySet(arr, arr + sizeof(arr) / sizeof(int));
//虽然set中只放value,但在底层实际存放的是由()构成的键值对,
//第一个value表示元素的值,第二个value表示元素出现的次数
cout << mySet.size() << endl;//输出元素个数
cout << mySet.count(7) << endl;//输出7的元素出现次数
//迭代器遍历元素
for (auto it = mySet.begin(); it != mySet.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
//返回迭代器遍历元素
for (auto rit = mySet.rbegin(); rit != mySet.rend(); ++rit)
{
cout << *rit << " ";
}
cout << endl;
// 查找元素
auto it = mySet.find(2);
if (it != mySet.end())
cout << "Found element: " << *it << endl;
else
cout << "Element not found" << endl;
// 删除元素
mySet.erase(5);
// 判断元素是否存在
if (mySet.count(5) > 0)
cout << "Element exists" << endl;
else
cout << "Element does not exist" << endl;
set::iterator itlow, itup;
itlow = mySet.lower_bound(3); // >=
itup = mySet.upper_bound(8); // >
cout << *itlow << endl;
cout << *itup << endl;
mySet.erase(itlow, itup);//删除这段区间的值
for (auto e : mySet)
cout << e << " ";
cout << endl;
//pair equal_range(const value_type & val) const;
//equal_range的函数原型, pair 这个是返回类型,后面介绍,实际上是个类模板。
auto ret = mySet.equal_range(1);
itlow = ret.first;
itup = ret.second;
cout << *itlow << endl;
cout << *itup << endl;
//如果参数是1输出的是1和2,如果参数是2,输出的是2和9,从结果知道equal_range()相当于lower_bound()和upper_bound()的组合
//返回的是第一个 >= 元素的值,第二个是 > 第一个下一个元素的值,实际不是给set使用的
//而是给下面的multiset使用的
}
void test_multiset()
{
//multiset只有排序,允许冗余
multiset s;
s.insert(3);
s.insert(3);
s.insert(1);
s.insert(1);
s.insert(8);
s.insert(9);
s.insert(8);
s.insert(2);
for (auto e : s)
{
cout << e << " "; //1 1 2 3 3 8 8 9
}
cout << endl;
//返回中序遍历的第一个3
auto pos = s.find(3);
while (pos != s.end())
{
cout << *pos << " ";
++pos;
}
cout << endl;
cout << s.count(3) << endl;//count也是为了multiset准备的
//因为set不存在一段重复连续的区间,而multiset存在一段连续重复的区间,可以使用equal_range对这段区间进行删除
auto ret = s.equal_range(3);//如果该值小于容器里最大元素并且不存在则返回一个不存在的区间,否则报错
auto itlow = ret.first;
auto itup = ret.second;
cout << *itlow << endl;
cout << *itup << endl;
s.erase(itlow, itup);
for (auto e : s)
{
cout << e << " ";//1 1 2 8 8 9
}
cout << endl;
//对比删除之后的结果,里面的3全部删除了
//除了find,count,equal_range,set和multiset的其他函数使用方法可以说是一样的
}
int main()
{
//test_set();
test_multiset();
return 0;
}
set和multiset的区别:
C++中的set和multiset都是关联容器,用于存储和操作一组按照特定规则排序的唯一元素。它们之间的主要区别在于元素的唯一性和插入顺序。
1. 唯一性:set中的元素是唯一的,每个元素只能出现一次。而multiset允许多个元素具有相同的值,即允许重复元素存在。
2. 插入顺序:set按照元素的键值自动进行排序,插入元素时会根据键值的顺序将元素插入到正确的位置。multiset也按照元素的键值自动进行排序,但是可以插入具有相同键值的元素,并按照插入的顺序进行存储。
3. 查找和访问:set和multiset都支持快速查找和访问操作,可以根据键值快速查找特定的元素。
4. 删除操作:set和multiset都支持删除元素的操作,但是set只能删除指定键值的第一个元素,而multiset可以删除所有具有指定键值的元素。
总结:set适用于需要存储唯一值的情况,且对元素的顺序有要求。multiset适用于需要存储重复值的情况,且对元素的顺序有要求。
为什么equal_range是给multiset准备的?
equal_range函数是用来获取某个键值范围的函数。对于set容器来说,由于元素是唯一的,所以对于一个给定的键值,要么存在一个元素与之匹配,要么不存在。因此,对于set容器来说,equal_range函数返回的范围要么是一个元素要么是空范围。
而对于multiset容器来说,由于允许元素重复,所以对于一个给定的键值,可能存在多个元素与之匹配。equal_range函数就是为了方便地返回匹配范围而设计的。它返回一个pair对象,包含两个迭代器,第一个迭代器指向范围中第一个匹配元素的位置,第二个迭代器指向范围中最后一个匹配元素的下一个位置。
因此,multiset容器中的元素可以通过equal_range函数进行更加方便的查找和操作,而set容器中由于元素的唯一性,equal_range函数的返回结果只有两种情况:找到匹配元素或者未找到匹配元素。
1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
2. 在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair : typedef pair
3. 在内部,map中的元素总是按照键值key进行比较排序的。
4. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
5. map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
6. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。
在使用C++中的map容器时,需要注意以下事项:
1. 区分键和值的类型:map是一种关联容器,它存储的是一对键值对。在定义map时,需要明确指定键和值的数据类型,并确保它们的类型匹配。
2. 确保键的唯一性:map要求每个键必须具有唯一性。当插入键值对时,如果键已经存在于map中,插入操作将不会成功。因此,在使用map时,需要确保键的唯一性,可以使用count()函数或find()函数来检查键是否已经存在。
3. 按键的顺序进行存储:map中的键值对是按照键的大小进行排序存储的。这使得在map中查找或迭代键值对时更加高效。但是需要注意的是,map并不是按照插入的顺序存储键值对的,而是按照键的大小进行排序的。
4. 使用迭代器进行遍历和操作:map提供了迭代器,可以使用迭代器来遍历map中的键值对,或者进行插入、删除和修改操作。在使用迭代器时,需要注意在修改map时可能会导致迭代器失效的情况,应该及时更新和处理迭代器。
5. 使用合适的比较函数:map默认使用小于运算符(<)进行键的比较,如果键的类型不支持小于运算符,需要自定义比较函数。在定义map时,可以通过函数对象或lambda函数来自定义比较函数,以确保正确的键的比较。
6. 了解复杂度:map的插入、删除和查找操作的平均复杂度是O(log n),其中n是元素的数量。这使得map适用于在较大规模数据集上进行高效的查找和操作。但是,需要注意在插入、删除和查找大量元素时,复杂度可能会影响性能。
总之,在使用C++中的map容器时,需要注意键的唯一性、按键的顺序存储、使用迭代器进行遍历和操作,选择合适的比较函数,以及了解操作的复杂度。这些注意事项可以帮助更好地使用和理解C++中的map容器。
key: 键值对中key的类型
T: 键值对中value的类型
Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)
Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器
注意:在使用map时,需要包含头文件
函数声明 |
功能介绍 |
map (const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type()); / map() |
构造一个空的map |
template map (InputIterator first, InputIterator last, const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type()); |
使用[first,last)区间中的元素构造map |
map (const map& x) |
拷贝构造函数 |
函数声明 |
功能介绍 |
begin()和end() |
begin:首元素的位置,end最后一个元素的下一个位置 |
cbegin()和cend() |
与begin和end意义相同,但cbegin和cend所指向的元素不能修改 |
rbegin()和rend() |
反向迭代器,rbegin在end位置,rend在begin位置,其++和--操作与begin和end操作移动相反 |
crbegin()和crend() |
与rbegin和rend位置相同,操作相同,但crbegin和crend所指向的元素不能修改 |
函数声明 |
功能介绍 |
bool empty ( ) const |
检测map中的元素是否为空,是返回true,否则返回false |
size_type size() const |
返回map中有效元素的个数 |
mapped_type& operator[] (const key_type& k) |
返回key对应的value |
问题:当key不在map中时,通过operator获取对应value时会发生什么问题?
当key不在map中时,通过operator获取对应value时会发生以下问题之一:
1. 如果使用`[]`操作符获取值,当key不存在时,会自动插入一个新的key-value对到map中,其中value的默认值会被返回。这可能会导致意外的数据修改,因为插入的新key-value对可能与预期不符。
2. 如果使用`.at()`成员函数获取值,当key不存在时,会抛出一个`std::out_of_range`异常。这可以让程序员意识到key不存在,并进行相应的错误处理。
需要根据具体的需求来选择合适的操作方式。如果不希望自动插入新的key-value对并需要明确处理不存在的key的情况,则应该使用`.at()`成员函数。如果允许自动插入新的key-value对,并且希望默认值被返回,可以使用`[]`操作符。
函数声明 |
功能介绍 |
pair (和set一样,不允许 key 被修改,但是它是一个键值对,可以修改value。) |
在map中插入键值对x,注意x是一个键值对,返回值也是键值对:iterator代表新插入元素的位置,bool代表释放插入成功 |
void erase ( iterator position ) |
删除position位置上的元素 |
size_type erase ( const key_type& x ) |
删除键值为x的元素 |
void erase ( iterator first, iterator last ) |
删除[first, last)区间中的元素 |
void swap ( map |
交换两个map中的元素 |
void clear ( ) |
将map中的元素清空 |
const_iterator find ( const key_type& x ) const |
在map中插入key为x的元素,找到返回该元素的位置的迭代器,否则返回end |
const_iterator find ( constkey_type& x ) const |
在map中插入key为x的元素,找到返回该元素的位置的const迭代器,否则返回cend |
size_type count ( const key_type& x ) const |
返回key为x的键值在map中的个数,注意map中key是唯一的,因此该函数的返回值要么为0,要么为1,因此也可以用该函数来检测一个key是否在map中 |
mapped_type& operator[] (const key_type& k) |
#include
#include
map和multimap的区别:
map和multimap是C++标准库提供的两种关联容器,它们的主要区别如下:
1. 键的唯一性:
map容器中的键是唯一的,每个键只能与一个值关联。如果尝试插入已经存在的键,则旧的键值对将被替换。
multimap容器中的键允许重复,每个键可以与多个值关联。可以插入多个具有相同键的键值对。
2. 元素的排序:
map容器中的元素按照键的大小进行排序,默认情况下使用键的比较函数或操作符进行排序。
multimap容器中的元素按照键的大小进行排序,但允许具有相同键的元素按照插入的顺序保持多个副本。
3. 存储结构:
map容器使用平衡二叉搜索树(通常是红黑树)来实现,这样可以保持元素的有序性。
multimap`容器也使用平衡二叉搜索树来实现。
4. API和功能:
map和multimap容器均提供了类似的API,包括插入、查找、删除等操作。
map容器还提供了operator[]运算符,可以通过键直接访问值,而multimap没有提供这个运算符。
综上所述,map适用于需要唯一键和排序的场景,而multimap适用于需要允许重复键和排序的场景。根据具体的需求,选择适合的关联容器可以提高代码的效率和可读性。
(API是Application Programming Interface(应用程序编程接口)的缩写,指的是一组定义了软件组件(函数、方法、类、对象等)之间交互的规范和约定。API定义了组件之间如何通信、调用和共享数据,提供了一种编程接口,使得开发者能够利用已经构建好的组件来实现自己的应用程序。
API可以是操作系统、软件库、框架或服务等的一部分。通过使用API,开发者可以通过调用提供的函数或方法来使用已经构建好的组件的功能,而无需了解其内部的具体实现细节。
API提供了一种抽象层,隐藏了底层的复杂性,并提供了常用操作的简化接口。它不仅可以简化开发过程,还可以促进代码的重用和模块化,提高软件的可靠性和可维护性。开发者可以根据API的规范和文档来正确地使用提供的功能,并根据需要进行自定义扩展。)
int main()
{
map myMap;
myMap.insert(make_pair(1, "apple"));
myMap.insert(make_pair(2, "banana"));
myMap.insert(make_pair(3, "cherry"));
myMap.insert(make_pair(2, "pear")); // 键2已经存在,将替换值
multimap myMultimap;
myMultimap.insert(make_pair(1, "apple"));
myMultimap.insert(make_pair(2, "banana"));
myMultimap.insert(make_pair(3, "cherry"));
myMultimap.insert(make_pair(2, "pear")); // 键2允许重复,插入多个值
std::cout << "Map:\n";
for (const auto& pair : myMap)
{
std::cout << pair.first << ": " << pair.second << std::endl;
}
std::cout << "\nMultimap:\n";
for (const auto& pair : myMultimap)
{
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
结论:
1.multimap允许插入多个相同键值的键值对
2.没有重载operator[]。为什么没有重载?
multimap没有重载operator[]的主要原因是,由于键允许重复,使用operator[]来访问值的操作会变得复杂。
当使用operator[]时,我们希望通过键直接访问与之关联的值。在map中,由于键的唯一性,我们可以直接使用operator[]来进行访问,因为每个键只有一个关联的值。但在multimap中,一个键可以对应多个值,使用operator[]时就无法确定应该返回哪一个值。
如果multimap重载了operator[],可能会有两种可能的行为:
因此,在multimap中没有重载operator[],而是提供了其他方法来访问和操作它的元素。