Set和Multiset都会根据特定的排序准则,自动将元素排序,两者不同在于multiset允许元素重复,而set不允许元素重复。
Set和Multiset的使用均需要包含头文件
#include
在这个头文件中,上述两个类型都被定义为std命名空间中的class template:
template<
typename Key,
typename Compare = std::less<Key>,
typename Allocator = std::allocator<Key>
> class set;
template<
typename Key,
typename Compare = std::less<Key>,
typename Allocator = std::allocator<Key>
> class multiset;
必须是能够比较的对象,才能作为set或者multiset的元素(即所谓的comparable)。排序准则默认为less——这是个函数对象,以operator<
对元素进行比较。第三个参数指定内存模型,默认是allocator
,由C++标准库提供。
注意——
所谓的“排序准则”,必须定义strick weak ordering,其定义如下:
1. 必须是非对称的(anisymmertric)
对operator<
而言,如果x < y为true,那么y < x 必为false。
对判别式 op()
而言,如果op(x,y)为ture,那么op(y,x)必须为false。
2. 必须是可传递的(transitive)
即,如果x
即,x < x永远为false
4. 必须具有等效传递性(transitivity of equivalence)
即,如果a等于b且b等于c,那么a必然等于c。
规则1~4的目的是保证set和multiset元素能够区分less
和equal
的关系,即<和=的关系。而区分两者的关系则是为了辨别元素是否重复——如果两个元素x和y,x
与所有的关联式容器类似,set和multiset通常以平衡二叉树完成——通常是红黑树,红黑树在改变元素数量和元素搜索方面具有出色的性能,它保证节点安插最多做两个重新链接的动作,而到达某一元素的最长路径的深度,最多是最短路径深度的两倍。
set和multiset具有自动排序的功能,自动排序使得平衡二叉树在查找元素时具有卓越的效率。但同时也带来一种限制——set和multiset无法直接改变元素的值,因为这样会打乱原本的顺序。
set和multiset本身并不提供任何操作函数可以直接访问元素,从迭代器的角度而言,元素的值是常量。因此,要改变元素的值,只能删除再插入。
set的构造函数和析构函数,如下表所示——
序号 | 操作 | 效果 |
---|---|---|
1 | set c | Default构造函数,产生一个空set,没有任何元素 |
2 | set c |
建立一个空的set,并以op为排序规则 |
3 | set c(c2) set c=c2 |
Copy构造函数,建立c2同型set并成为c2的一份副本,该复制是深度复制 |
4 | set c(rv) set c=rv |
Move构造函数,rv是一个set右值引用,那么这里的构造函数是一个Move构造函数,建立一个新的set,取右值内容(C++11新特性) |
5 | set c(beg,end) | 建立一个set,并以迭代器所指向的区间[beg,end)作为元素值,该构造函数支持从其他容器类型接收元素 |
6 | set c(beg,end,op) | 建立一个set,并以迭代器所指向的区间[beg,end)作为元素值,同时以op作为排序准则 |
7 | set c(initlist) set c=initlist |
建立一个set,以初值列initlist元素为初值(C++11新特性) |
8 | c.~set() | 销毁所有元素,释放内存 |
其中,上表中set的形式可以置换为以下任意一种——
序号 | set | 效果 |
---|---|---|
1 | set |
使用默认排序准则,以Elem作为元素类型的set |
2 | set |
使用op作为排序准则,以Elem作为元素类型的set |
3 | multiset |
使用默认排序规则,以Elem作为元素类型的multiset |
4 | multiset |
使用op作为排序准则,以Elem作为元素类型的multiset |
#include
#include
using namespace std;
int main(){
cout << "multiset:" << endl;
multiset<string> ms{ "B","C","A","A","A" };
for (auto&it : ms) {
cout << it << " ";
}
cout << endl << "set:" << endl;
set<string> s{ "B","C","A","A" };
for (auto&it : s) {
cout << it << " ";
}
return 0;
}
上述代码中,使用range-based for循环对set/multiset的所有元素进行遍历,显然——
1.set/multiset具有自动排序功能;
2.set不允许重复元素,尽管使用初值列初始化有重复元素,但是即不生效,也不报错。
Set也提供元素比较、查询大小等操作——
序号 | 操作 | 效果 |
---|---|---|
1 | c.empty() | 容器为空返回true,不为空返回false,相当于size()==0 |
2 | c.size() | 返回当前元素的个数 |
3 | c.max_size() | 返回元素个数之最大可能量 |
4 | c1==c2 | 对每个元素调用c1==c2,全部相等返回true |
5 | c1!=c2 | 只要有一个元素相等,返回true,相当于!(c1==c2) |
6 | c1>c2,c1>=c2,c1同上,依次类推 |
|
7 | c.key_comp() | 返回比较准则 |
8 | c.value_comp() | 返回针对value的比较准则,但实际上,Set中并没有Key-Value之说,所以,此处的value比较准则与key比较准则相同(Map中是不同的) |
set的元素比较只适用于类型相同的元素,这里的类型相同包括元素类型和排序准则,在set中,排序准则是作为类型看待的,因此不同排序类型的set无法比较,比如——
std::set<float> c1;
std::set<float,std::greater<float>> c2;
if(c1==c2)// 编译错误
{
...
}
Set和Multiset在元素查找方面具有优化设计,因此提供了特殊的查找函数,
序号 | set | 效果 |
---|---|---|
1 | c.count(val) | 返回元素为val的个数 |
2 | c.find(val) | 返回元素为val的第一个位置,注意这里返回的是一个迭代器 |
3 | c.lower_bound(val) | 返回val第一个可插入的位置,即第一个元素值<=val的位置 |
4 | c.upper_bound(val) | 返回val最后一个可插入的位置,即第一个元素值 |
5 | c.equal_range(val) | 返回val可以被插入的第一个位置和最后一个位置的范围 |
通过一个例子暂时其查找函数的能力——
multiset<int> ms{ 1,1,1,2,2,2,3,3,4,5,6,7,8 };
cout << "ms.count(1) = " << ms.count(1) << endl;
multiset<int>::iterator it1 = ms.find(2);
cout << "ms.find(2) :" << endl << *it1 << endl;
pair<multiset<int>::iterator,multiset<int>::iterator> it2 = ms.equal_range(3);
cout << "ms.equal_range(3) :" << endl << *it2.first << " " << *it2.second << endl;
其中,除了count是返回int型之外,其余的查找函数返回的都是set
迭代器,其指向的位置是已构建的平衡二叉树的位置,而不是初始化时的位置。
Set只提供任何容器都提供的基本赋值操作:
序号 | 操作 | 效果 |
---|---|---|
1 | c = c2 | 将c2的全部元素赋值给c |
2 | c = rv | 将rv右值语义所有元素以Move assign的方式赋值给c(C++11新特性) |
3 | c = initlist | 将初值列所有元素赋值给c(C++11新特性) |
4 | c1.swap(c2) swap(c1,c2) |
置换c1和c2的数据 |
Set和Multiset不提供元素访问,所以只能采用range-based for循环,迭代器的使用就尤为常见:
序号 | 操作 | 效果 |
---|---|---|
1 | c.begin() | 返回一个bidirectional iterator指向第一个元素 |
2 | c.end() | 返回一个bidirectional iterator指向的之后一个元素 |
3 | c.cbegin() | 返回一个const bidirectional iterator指向的第一个元素(C++11新特性) |
4 | c.cend() | 返回一个const bidirectional iterator指向的最后一个元素(C++11新特性) |
5 | c.rbegin() | 返回一个反向迭代器(reverse iterator)指向的第一个元素 |
6 | c.rend() | 返回一个reverse iterator指向的最后一个元素 |
7 | c.crbegin() | 返回一个const reverse iterator指向的第一个元素(C++新特性) |
8 | c.crend() | 返回一个const reverse iterator指向的最后一个元素(C++11新特性) |
multiset<string> ms{ "apple","xiaomi","huawei","vivo","oppo" };
// 遍历set常用range-based for循环
for (auto&elem : ms) {
cout << elem << " ";
}
cout << endl;
// 或者使用迭代器
for (auto it = ms.cbegin(); it != ms.cend(); it++) {
cout << *it << " ";
}
对Set和Multiset而言,这里的迭代器只能是双向迭代器,因此,对于那些只能够支持“随机访问迭代器”的STL算法(如random shuffling),Set和Multiset无福消受。
更重要的是,从迭代器的角度而言,Set和Multiset的元素都被视为常量,这是因为改变其元素值会打乱既有顺序,因此无法通过迭代器改变其元素值,也无法对Set和Multiset采用任何易更型算法,只能通过其提供的成员函数来改变其元素值。
Set的所有插入和移除操作如下表所示——。
序号 | 操作 | 效果 |
---|---|---|
1 | c.insert(val) | 插入val元素,并返回新元素的位置,不论是否成功 |
2 | c.insert(pos,val) | 在iterator指向的pos位置的前方插入一个元素val的副本,并返回新元素的位置(pos只是一个提示,该提示恰当会加快查找过程) |
3 | c.insert(beg,end) | 插入区间[beg,end)内所有元素的副本,无返回值 |
4 | c.insert(initilist) | 插入initilist的所有元素的副本,无返回值(C++11新特性) |
5 | c.emplace(args…) | 插入一个以args为初值的元素,并返回新元素的位置,无论是否成功(C++11新特性) |
6 | c.emplace_hint(pos,args…) | 插入一个以args为初值的元素,并返回新元素的位置,pos是个位置提示,如果恰当,将大大提高插入效率 |
7 | c.erase(val) | 移除与val相等的所有元素,返回被移除的个数 |
8 | c.erase(pos) | 移除迭代器iterator指向的元素,无返回值 |
9 | c.erase(beg,end) | 移除区间[beg,end)内的所有元素,无返回值 |
10 | c.clear() | 移除所有元素,并将容器清空 |
下面通过一段程序展示set插入和删除的若干能力——
multiset<int> ms{ 1,2,3,4,5,6 };
ms.insert({ 7,8,9,10 });
for (auto it : ms) {
cout << it << " ";
}
初值列(initialization list) 是C++11的重要新特性,无论是初始化、赋值、插入,都变得十分简单。
注意,用以插入元素的函数insert和emplace,对于基本类型,两者没有太大区别,emplace不支持初值列,因此只能一个一个插入,而对于对象,insert是复制对象,调用的是复制构造函数,而emplace调用的是默认构造函数(或对应的自定义构造函数)。
此外,由于Set不允许元素重复,而Multiset可以,两者的insert函数有所不同——
// Set提供如下接口
pair<iterator,bool> insert(const value_type&val);
iterator insert(const_iterator posHint,const value_type&val);
template<typename args>
pair<iterator,bool> emplace(args);
template<typename args>
iterator emplace_hint(const_iterator posHint,const value_type&val);
// Multiset 提供如下接口
iterator insert(const value_type&val);
iterator insert(const_iterator posHint,const value_type&val);
template<typename args>
iterator emplace(args);
template<typename args>
iterator emplace_hint(const_iterator posHint,const value_type&val);
两者区别在于,Set不支持重复元素,因此返回的是一个pair组织起来的两个值,其中pair.second表示是否插入成功,pair.first表示插入元素之后的位置(如果值相同,则返回相同元素的位置)。
同时,C++11保证,Multiset的insert、emplace和erase操作都会保证元素间的相对次序,插入元素都会被放到既有等值元素的末尾。
Set和Multiset是以节点为基础的容器,如果节点构建失败,容器保持原样。此外,由于(元素)析构函数通常不抛出异常,因此移除元素不能失败。
对于单一元素的插入操作,能够保证其“要么成功,要么没有任何作用”的操作原则,但是对于多重元素的插入操作,必须保证其排序准则不抛出异常,才能保证其达到这一点。