set和multiset容器概述
- 首先set和multiset这两种容器底层是红黑树(是一种数据结构,是树形结构)实现的。
- 我们前面说到vector,deque容器是插入数据慢,但是读取数据快,list呢恰恰相反,插入数据快,查询慢。但是set和multiset其数据查询和插入速度都很快(由于底层是有红黑树实现)。
- 对于set我们依然没有办法使用下标[]或者at(),来访问数据,而且其迭代器和指针也只能进行++,--运算(前置和后置都行),!=,==的比较运算。(这个和list容器是类似的,因为在set中元素和元素之间也是通过指针来联系的,它们的内存也不挨着 -- 所以set和multiset也不需要提前开辟空间)
- set容器中存放的元素不能有重复的,但是multiset是可以存放相同元素的。
- set和multiset会对数据进行自动排序(我们可以指定排序规则)。
成员方法 | 功能 |
---|---|
begin() | 返回指向容器中第一个(注意,是已排好序的第一个)元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
end() | 返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
rbegin() | 返回指向最后一个(注意,是已排好序的最后一个)元素的反向双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
rend() | 返回指向第一个(注意,是已排好序的第一个)元素所在位置前一个位置的反向双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
find(val) | 在 set 容器中查找值为 val 的元素,如果成功找到,则返回指向该元素的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
lower_bound(val) | 返回一个指向当前 set 容器中第一个大于或等于 val 的元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
upper_bound(val) | 返回一个指向当前 set 容器中第一个大于 val 的元素的迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
equal_range(val) | 该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的值为 val 的元素(set 容器中各个元素是唯一的,因此该范围最多包含一个元素)。 |
empty() | 若容器为空,则返回 true;否则 false。 |
size() | 返回当前 set 容器中存有元素的个数。 |
max_size() | 返回 set 容器所能容纳元素的最大个数,不同的操作系统,其返回值亦不相同。 |
insert() | 向 set 容器中插入元素。 |
erase() | 删除 set 容器中存储的元素。 |
swap() | 交换 2 个 set 容器中存储的所有元素。这意味着,操作的 2 个 set 容器的类型必须相同。 |
clear() | 清空 set 容器中所有的元素,即令 set 容器的 size() 为 0。 |
emplace() | 在当前 set 容器中的指定位置直接构造新元素。其效果和 insert() 一样,但效率更高。 |
emplace_hint() | 在本质上和 emplace() 在 set 容器中构造新元素的方式是一样的,不同之处在于,使用者必须为该方法提供一个指示新元素生成位置的迭代器,并作为该方法的第一个参数。 |
count(val) | 在当前 set 容器中,查找值为 val 的元素的个数,并返回。注意,由于 set 容器中各元素的值是唯一的,因此该函数的返回值最大为 1。 |
先说set容器再说multiset容器,首先使用set或者multiset都需要导入头文件#include<set>
1. set的默认构造
代码
set<int> s1;
set<float> s2;
set<Student> s3;
set<int *> s4;
set<Student *> s5;
2. set的有参构造
代码
/*
vector<int> v1{10,11,12,13,14,15,16};
int arr[] = { 100,200,300,400,500 };
set<int> s1{ 1,2,3,4,5 };
set<int> s2(s1);
set<int> s3(s1.begin(),s1.begin()++);
set<int> s4(v1.begin(), v1.end());
set<int> s5(arr, arr + 3);
*/
相关知识点
- 和之前的容器没有什么区别。(set不能使用数字直接作为有参构造的参数)
3. set容器的元素个数
代码: 使用size()函数
int main(void) {
set<int> s1{10,11,12,13,14,15};
cout << "set 的元素个数: " << s1.size() << endl; // 6
system("pause");
return 0;
}
代码: 使用count()函数
int main(void) {
set<int> s1{10,11,12,13,14,15};
set<int>::size_type nub = s1.count(10); // 或者使用size_t
cout << "10出现的个数:" << nub << endl;
system("pause");
return 0;
}
结果
相关知识点
- count(elem); // 返回容器中元素elem的个数为多少?
- 注意: 因为set中存储的各个元素不能相同,所以count只能返回0或者1。对于multiset就可以大于1。
4. set的元素访问(有变动)
代码: 使用迭代器
int main(void) {
set<int> s1{10,11,12,13,14,15};
for (set<int>::iterator it = s1.begin(); it != s1.end(); it++) {
cout << *it << " "; // 10 11 12 13 14 15
}
cout << endl;
system("pause");
return 0;
}
相关知识点
- set无法使用下标进行访问元素,所以无法使用[]和()进行访问。
- set没有函数front()和back()函数。(可以这么理解,树形结构没有直观的先后顺序)
- 所以set只能使用迭代器访问元素。
5. set的迭代器
成员方法 | 功能 |
---|---|
begin() | 返回指向容器中第一个(注意,是已排好序的第一个)元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
end() | 返回指向容器最后一个元素(注意,是已排好序的最后一个)所在位置后一个位置的双向迭代器,通常和 begin() 结合使用。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
rbegin() | 返回指向最后一个(注意,是已排好序的最后一个)元素的反向双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
rend() | 返回指向第一个(注意,是已排好序的第一个)元素所在位置前一个位置的反向双向迭代器。通常和 rbegin() 结合使用。如果 set 容器用 const 限定,则该方法返回的是 const 类型的反向双向迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改容器内存储的元素值。 |
find(val) | 在 set 容器中查找值为 val 的元素,如果成功找到,则返回指向该元素的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
lower_bound(val) | 返回一个指向当前 set 容器中第一个大于或等于 val 的元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
upper_bound(val) | 返回一个指向当前 set 容器中第一个大于 val 的元素的迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。 |
equal_range(val) | 该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的值为 val 的元素(set 容器中各个元素是唯一的,因此该范围最多包含一个元素)。 |
注意,以上成员函数返回的迭代器,指向的只是 set 容器中存储的元素,而不再是键值对。另外,以上成员方法返回的迭代器,无论是 const 类型还是非 const 类型,都不能用于修改 set 容器中的值。
代码
int main(void) {
set<int> s1{10,11,12,13,14,15};
for (set<int>::iterator it = s1.begin(); it != s1.end(); it++) {
cout << *it << " "; // 10 11 12 13 14 15
// *it = 5; //错误操作,不能直接修改set中的元素值
}
system("pause");
return 0;
}
相关知识点
注意: set的迭代器只能进行++,--(前置或者后置)的运算操作,==,!=的比较运算。 以及不能使用迭代器修改set中的元素值。(无论迭代器有没有const修饰)
其余操作和前面介绍的容器一样。
6. set在指定位置插入元素
代码:使用insert()函数
int main(void) {
set<int> s1{ 10,11,12,13,14,15 };
set<int> s2{ 19,5 };
vector<int> v1{ 99,100 };
int arr[] = { 1,2,3 };
s1.insert(s2.begin(), s2.end());
s1.insert(arr, arr+3);
s1.insert(v1.begin(), v1.end());
s1.insert(4);
// s1.insert(4, 6); // 这样写会出错,因为set中不能有重复的元素
s1.insert({ 6 });
s1.insert(s1.begin(), 50);
for (set<int>::iterator it = s1.begin(); it != s1.end(); it++) {
cout << *it << " "; // 10 11 12 13 14 15
}
system("pause");
return 0;
}
结果
相关知识点
- insert(elem); // 将元素elem插入到set中去
- insert(ptr1,ptr2) ; // 将[ptr1,ptr2)指针指向的范围中的数据插入到set中去。(数组)
- insert(beg,end); // 将迭代器[beg,end)范围内的数据插入到set中去。(可以相同容器的迭代器,也可以是不同容器的)
- insert({data...}); // 将{}中的元素插入到set中
- insert(iter,elem); // 将元素elem插入到set中
注意: 无论是哪种插入,在元素插入到set中时,set都会进行比较,放到合适的位置。因为set是有序的。(尽管我们指定了插入位置,但数据并不一定会放到对应位置,它会根据排序规则来确定其放在哪个位置)
insert的返回值
情况一:
set<int> s1{1,2,3};
pair<set<int>::iterator,bool> p1 = s1.insert(5);
if (p1.second) {
cout << *(p1.first) << endl;
}
pair<set<int>::iterator, bool>p2 = s1.insert(5);
if (!p2.second) {
cout << "插入失败" << endl;
cout << *(p2.first) << endl;
}
相关知识点:
当我们直接插入单个数据的时候,对于set会存在插入失败的情况,因为set中的元素不能重复,如果我们插入的元素在set中已经存在就会插入失败。
那么我们怎么能判断是否插入成功了呢?
insert会给我们返回一个bool值,如果插入成功:返回true,如果插入失败:返回false。
同时insert还会返回一个迭代器,如果插入成功:迭代器指向插入元素的位置,如果插入失败:迭代器指向与插入值相同的元素位置。
上面代码中,我们分别展示了插入成功和插入失败的情况。
其它情况
set<int> s1;
set<int> s2{1,2,3};
s1.insert(s2.begin(), s2.end()); // 返回void
set<int>::iterator it = s1.insert(s1.begin(),5); // 返回指向插入元素位置的迭代器
cout << *it << endl;
s1.insert({ 10,11 }); // 返回void
- 在指定位置插入时 (但是,由于set是有序的,所以插入的数据会根据内部数据进行位置调整,不一定会放到我们指定的位置)。
- 如果插入成功: 返回插入元素位置的迭代器;如果插入失败: 返回与插入值相同的元素位置。
代码: 使用emplace()和emplace_hint()函数
set<int> s1;
// 使用emplace
pair<set<int>::iterator,bool> p1 = s1.emplace(); // 插入0,返回pair类对象
if (p1.second) {
cout << *(p1.first) << endl;
}
pair<set<int>::iterator, bool> p2 = s1.emplace(1); // 插入1,返回pair类对象
if (p2.second) {
cout << *(p2.first) << endl;
}
// 使用emplace_hint
set<int>::iterator it = s1.emplace_hint(s1.begin(), 10); // 在指定位置插入10,返回指向插入元素位置的迭代器
cout << *it << endl;
结果
相关知识点
- emplace()函数和之前介绍的用法是一致的,直接在()写相应的数据(类对象的话,就写相应的构造函数参数对应的数据。其它类型的话,直接写对应的数据)
其返回值是一个pair的类对象,和insert是一样的
返回一个迭代器 -> 如果插入成功: 指向插入数据的位置; 如果插入失败: 指向和插入数据相同的元素位置。
返回bool值 -> 如果插入成功: 返回true; 如果插入失败: 返回false。
- emplace_hint(pos,elem); // 在pos位置插入数据elem。 (但是,由于set是有序的,所以插入的数据会根据内部数据进行位置调整,不一定会放到我们指定的位置)
其返回值是一个迭代器,如果插入成功: 指向插入数据的位置; 如果插入失败: 指向和插入数据相同的元素位置。
7. set删除元素
代码: 使用erase()和clear()
int main(void) {
set<int> s1{10,11,12,13,14,15};
// 用法1
set<int>::size_type t1 = s1.erase(10);
cout << t1 << endl;
// 用法2
s1.erase(s1.begin(), ++s1.begin());
// 用法3
s1.erase(s1.begin());
s1.clear(); // 清空set中的元素
for (set<int>::iterator it = s1.begin(); it != s1.end(); it++) {
cout << *it << " ";
}
cout << endl;
system("pause");
return 0;
}
相关知识点
- erase(elem); // 删除set列表中值为elem的元素,返回删除元素的个数。(对于set只会返回0或者1,但是multiset可能会大于1)
- erase(pos); // 删除对应位置中的元素, 返回删除元素下一个元素的迭代器。
- erase(beg,end); // 删除[beg,end)范围内的元素,返回删除元素下一个元素的迭代器。
- clear(); // 删除set容器中所有的元素。
注意: 在erase和循环结合使用的时候,应该注意删除时迭代器的指向。(在vector中详述)
8. set查找元素
代码: 使用find()函数
int main(void) {
set<int> s1{10,11,12,13,14,15};
//set<int>::const_iterator cit = s1.find(10);
set<int>::iterator it = s1.find(10);
if (it != s1.end()) {
cout << *it << endl;
}
else {
cout << "没有找到此元素" << endl;
}
system("pause");
return 0;
}
结果
相关知识点
- find(elem); // 用于查找set中elem元素。
- 如果此元素存在,则返回该元素位置的迭代器;如果此元素不存在,则返回最后一个元素下一位的迭代器。(和end()函数返回的迭代器一样)
所以在代码中,我们先判断find返回的迭代器和end()返回的迭代器是否相同,相同: 则输出元素值;不相同: 则输出查找失败。
代码: lower_bound(),upper_bound(),equal_range()函数
int main(void) {
set<int> s1{10,11,12,13,16,17};
// lower_bound()
//set<int>::const_iterator it = s1.lower_bound(10);
/*
set<int>::iterator it = s1.lower_bound(10);
if (it != s1.end()) {
cout << *it << endl; // 10
}
else {
cout << "没有此元素" << endl;
}
*/
set<int>::iterator it = s1.lower_bound(16);
if (it != s1.end()) {
cout << *it << endl; // 16
}
else {
cout << "没有此元素" << endl;
}
// upper_bound()
set<int>::iterator it1 = s1.upper_bound(12);
if (it1 != s1.end()) {
cout << *it1 << endl; // 13
}
else {
cout << "没有此元素" << endl;
}
// equal_range()
pair<set<int>::iterator, set<int>::iterator> p1 = s1.equal_range(10);
for (; p1.first != p1.second; (p1.first)++) {
cout << *(p1.first) << endl;
}
system("pause");
return 0;
}
结果
相关知识点
- lower_bound(elem); // 返回set中元素>=elem的第一个元素的迭代器,如果set中没有比elem>=的元素,那么就返回最后一个元素下一个位置的迭代器。(和end()返回的一样)
- upper_bound(elem); // 返回set中元素>elem的第一个元素的迭代器,如果set中没有比elem>的元素,那么就返回最后一个元素下一个位置的迭代器。(和end()返回的一样)
- equal_range(elem); // 返回一个pair对象,内部有两个迭代器类型,第一个迭代器: 表示第一个等于elem元素的位置,第二个迭代器: 表示最后一个等于elem的下一个元素的位置。(set中因为元素不相同,所以这个范围最多包含一个元素)
- 如果elem不存在,那么两个迭代器就都指向按照规则elem的下一个元素,(如果从小到大排列就指向第一个elem的元素)。 当然如果按照对则,elem排在最后,则两个迭代器都指向最后一个元素的下一位。(比如: 从小到大排时,elem比set中的元素都要大)
举个例子,lower_bound(elem); 要找到是>=elem的数据(并且排序是从小到大排),如果数据为elem的值不存在,那么返回的两个迭代器,就会指向第一个键值>elem的元素。
9. set的排序规则
我们前面说到,set是有顺序的,而且在我么插入元素,查找元素等操作中,都会根据排序规则进行相应的调整。
那set的排序规则到底是怎么样的呢?
set内部使用的是类模板,我们定义set对象的时候,会传入一个类型参数。 其实set还有第二个类型参数,用于指定set的排序规则。(类型为什么可以指定排序规则,这是因为类内部存在一个()函数的重载,没错就是函数对象)
那为什么上面的代码中我们不需要传入第二个类型参数呢?
因为第二个类型参数,有一个默认值为less<类型>,就是内部定义的函数对象类less,其指定的规则是从小到大排序。 所以在默认情况下set是以从小到大的情况进行排序的。
当然,我们也可以自己指定排序规则
代码: 使用greater使set从大到小和自定义的函数对象排列数据
int main(void) {
set<int> s1{10,11,12,13,16,17};
cout << "默认情况下输出: " << endl;
for (set<int>::iterator it = s1.begin(); it != s1.end(); it++) {
cout << *it << " ";
}
cout << endl;
set<int,greater<int>> s2{ 10,11,12,13,16,17 };
cout << "greater规则下输出: " << endl;
for (set<int>::iterator it = s2.begin(); it != s2.end(); it++) {
cout << *it << " ";
}
cout << endl;
system("pause");
return 0;
}
结果
相关知识点
代码中,我们分别使用了默认情况(less -- 从小到大排序),greater -- 从大到小排序。它们打印出的顺序或者元素是不相同的。
10. set的其它方法
方法: swap(), empty(), max_size();
和vector中的使用方法一样。
11. multiset与set的函数不同之处
multiset和set的函数大多数在使用的时候都是一样的,我们就不再说明了。
但是由于multiset可以存放不同的元素所以和set会有点区别。
区别:
- multiset在调用erase(elem),方法时期返回值可以大于1。
- 同理count(elem)这个函数的返回值也可以大于1。
- lower_bound(elem)、upper_bound(elem)、equal_range(elem)其对应查找的数据范围也不再是一个,所以这些函数更加常用于multiset。
注意事项:
- 由于set内部是有序的,所以我们不能随便修改其内部的值,因为如果修改了某一位置的值,很可能会导致其不再是有序排列的了。所以使用迭代器(即使迭代器不是const修饰的)或者其它方式都不能对set中的元素进行修改。如果要修改,那必须将这个元素删除,然后再添加一个新元素。
- size_type和value_type和vector的用法一样。(其它内嵌类型也一样)
- multiset是类似的。
注意,虽然 multiset 容器和 set 容器拥有的成员方法完全相同,但由于 multiset 容器允许存储多个值相同的元素,因此诸如 count()、find()、lower_bound()、upper_bound()、equal_range()等方法,更常用于 multiset 容器。