基于C++11的Set和Multiset容器分析

Set和Multiset的基本能力

  1. set和multiset会根据特定的排序准则,自动将元素排序。两者不同之处在于multiset允许元素重复而set不允许

  1. 在使用set和multiset之前,必须先包含头文件<set>

  1. 以上者两个类型都被定义为命名空间std内的class template:

namespace std{
    template<typename T,typename Compare =less<T>,typename Allocator =allocator<T>>
       class set;
    template<typename T,typename Compare =less<T>,typename Allocator =allocator<T>>
       class multiset;

只要是可依据某排序准则被比较的任意类型T都可以成为set或multiset的元素类型。第二个实参默认为less<T>函数对象,以operator <进行元素比较。第三个实参默认为allocator,由C++标准库提供,定义内存模型。

所谓"排序准则",必须定义strict weak ordering,其意义如下:

  1. 必须是非对称的:对于operator <而言,如果x<y为true,则y<x为false

  1. 必须是可传递的:对于operator <而言,如果x<y为true且y<z为true,则x<z为true

  1. 必须是非自反的:对于operator <而言,如果x<x永远为false

  1. 必须有等效传递性:如果a等于b且b等于c,那么a必然等于c

这个"强弱规则"告诉我们必须要区分less和equal。一个像operator <=并不满足这个条件。

根据这些性质,排序准则也被用来检查等效性。也就是说,两个元素如果没有任何一个小于另一个,则它们被视为重复。

Multiset的等效元素的次序是随机但稳定的。因此C++11保证安插和抹除动作都会保存等效元素的相对次序。

Set和Multiset的能力

和所有标准的关联式容器类似,set和multiset通常以平衡二叉树完成--C++standard并未规定。(事实上,这两个容器通常以红黑树实现)

自动排序的主要优点在于令二叉树用于查找元素时拥有良好性能(即AVL树的优点)。

但是自动排序造成一个重要限制:你不能直接改变元素值(改变const key的值),因为这个会打乱原本正确的顺序。

因此你如果要改变元素的值,必须先删除旧元素的,再插入新元素。

Set和Multiset的操作函数

创建,复制和销毁(Create,Copy,and Destroy)

set c//Default构造函数,建立一个空的容器
set c(op)//建立一个空的容器,以op作为排序准则
set c(c2)//Copy构造函数,将相同类型c建立为c2的一份拷贝,所有元素均为拷贝
set c=c2//Copy构造函数,将相同类型c建立为c2的一份拷贝,所有元素均为拷贝
set c(rv)//Move构造函数,取rv的内容建立c的内容(类型相同)
set c=rv//Move构造函数,取rv的内容建立c的内容(类型相同)
set c(beg,end)//以区间[begin,end)内元素为初值,建立一个容器
set c(beg,end,op)//以区间[begin,end)内元素为初值,并以op为排序准则,建立一个容器
set c(initlist)//以初始化列表的元素为初值,建立一个容器
set c=initlist//以初始化列表的元素为初值,建立一个容器
c.~set()//销毁所有元素,释放内存

其中set可被视为以下形式:

set<T>//一个set,以less<>为排序准则
set<T,op>//一个set,以op()为排序准则
multiset<T>//一个multiset,以less<>为排序准则
multiset<T,op>//一个multiset,以op()为排序准则

有两种方式可以定义排序准则:

  1. 以template参数定义之。例如:

std::set<int,std::greater<int>>coll;

这种情况下排序准则就是类型的一部分。于是类型系统确保"只有排序准则相同的容器才能被合并"。

  1. 以构造函数参数定义之。这种情况下,同一个类型可以运用不同的排序准则,而排序准则的初始值或状态也可以不同。如果运行期才获得排序准则,而且需要用到不同的排序准则,这一方式可以排上用场。

以下是运行期确定排序准则的例子:

#include<iostream>
#include<set>
using namespace std;

class Runtimecmp{     //自定义一个函数对象,用于比较,通过枚举类的切换,得到运行期判断
public:enum cmp_mode{normal,reverse};
private:cmp_mode mode;
public:
   Runtimecmp(cmp_mode m=normal):mode(m){}
   template<typename T> 
   bool operator ()(const T&t1,const T&t2)const{
        return mode==normal?t1<t2:t2<t1;
   }
   bool operator ==(const Rumtimecmp&rc){
        return mode==rc.mode;
   }
};
using IntSet=set<int,Rumtimecmp>;

int main(){
   IntSet coll1={4,7,5,1,6,2,5};
   cout<<"coll1: ";
   for(auto it:coll1)cout<<it<<" ";
   cout<<endl;
  
   Rumtimecmp reverse_order(Rumtimecmp::reverse);

   IntSet coll2(reverse_order);
   coll2={4,7,5,1,6,2,5};
   cout<<"coll2: ";
   for(auto it:coll2)cout<<it<<" ";
   cout<<endl;

   coll1=coll2;  //这个赋值要注意,如果排序准则不相同,不能直接赋值
   coll1.insert(3);
   cout<<"coll1: ";
   for(auto it:coll1)cout<<it<<" ";
   cout<<endl;
}

程序结果输出如下:

coll1: 1 2 4 5 6 7
coll2: 7 6 5 4 2 1
coll1: 7 6 5 4 3 2 1

在这个程序中,class Runtimecmp提供了一种泛化的能力:允许在运行期间对任何数据类型指定排序准则。其default构造函数设定以升序进行排序,用到是默认值normal。它也允许你使用Rumtimecmp::reverse作为构造函数实参,导致以降序排序。

如果用户没有提供特定的排序准则,就采用默认准则--函数对象less<>,less<>通过operator <对元素进行排序。

此时两元素的相等性检验如下:

if(!(elem1<elem2||elem2<elem1))

这样有三个好处:

  1. 只需传递一个实参作为排序准则

  1. 不必针对元素类型提供operator ==

这样的相等性判断可能会消耗较多时间,因为可能需要两次比较。

非更易型操作(Nonmodifying Operation)

c.key_comp()//返回比较准则
c.value_comp()//返回针对value的"比较准则"(和key_comp相同)
c.empty() //返回是否容器为空(相当于size()==0但也许较快)
c.size()  //返回元素的数量
c.max_size() //返回元素个数之最大可能量
c1==c2   //返回c1是否等于c2
c1!=c2   //返回c1是否不等于c2
c1>c2    //返回c1是否大于c2
c1<c2    //返回c1是否小于c2
c1>=c2   //返回c1是否大等于c2
c1<=c2   //返回c1是否小等于c2

注:容器的比较只适用于类型相同的容器。如果元素和排序准则不同的时进行比较,在编译期会产生类型方面的错误。

std::set<float>c1;
std::set<float,std::greater<float>>c2;
if(c1==c2)  //ERROR:类型不同

比较动作是以"字典顺序"进行比较某个容器是否小于另一个容器。

特殊的查找函数(Special Search Operation)

set和multiset在元素快速查找方面有优化设计,提供了同名STL算法的特殊版本,可以获得对数复杂度。

c.count(val)//返回元素值为val的个数
c.find(val)//返回元素第一个值为val的位置,找不到就返回end()
c.lower_bound(val)//返回元素值>=val的第一个元素的位置(第一个可安插位置)
c.upper_bound(val)//返回元素值>val的第一个元素的位置(最后一个可安插位置)
c.equal_range(val)//返回元素值==val的区间,(第一个和最后一个可安插位置)

赋值(Assignment)

c=c2              //将c2的全部元素赋值给c
c=rv              //将rv的所有元素都已move assign的方式给予c
c=initlist        //将初始值列表的所有元素赋值给c
c1.swap(c2)       //置换c1和c2的值
swap(c1,c2)       //置换c1和c2的值

这些操作函数,赋值操作两端的容器必须具有相同类型。尽管"比较准则"本身可能不同,但其类型必须相同。如果准则不同,准则本身也会联同容器一并被赋值或交换。

迭代器相关函数(Iterator Function)

c.begin()      //返回一个指向首处 bidirectional iterator
c.cbegin()     //返回一个const bidirectional iterator
c.end()        //返回一个指向末尾下一个位置的 bidirectional iterator
c.cend()       //返回一个指向末尾下一个位置的const bidirectional iterator
c.rbegin()     //返回一个reverse bidirectional iterator,指向末尾的下一个位置
c.rend()       //返回一个reverse bidirectional iterator,指向首处的位置
c.crend()      //返回一个const reverse bidirectional iterator,指向首处的位置
c.crbegin()    //返回一个const reverse bidirectional iterator,指向末尾的下一个位置

这里的迭代器类型为双向迭代器。对于只接受随机访问迭代器的STL算法,该容器便无福消受了。重要的是,由于所有的元素都被视为常量,因此你无法调用任何更易型算法。如果你想移除其中的元素,你只能使用它们所提供的成员函数。

元素的安插和移除(Inserting and Removing)

c.insert(elem)//安插一个elem的拷贝,返回新元素的位置
c.insert(pos,elem)//插入elem的一个拷贝,并且返回新元素的位置(pos只是起到了提示和加快速度的参数)
c.insert(beg,end)//插入[begin,end)区间的拷贝,无返回值
c.insert(initlist)//插入初始化列表的拷贝,无返回值
c.emplace(args...)//插入以args作为初值的元素,并且返回新元素的位置
c.erase(pos)//移除pos位置上的元素,无返回值
c.erase(val)//移除值为val的所有元素,返回被移除元素的个数
c.erase(beg,end)//移除区间[beg,end)内的所有元素,无返回值
c.clear()//移除所有元素,将容器清空

注意,用以安插元素的函数:insert()和emplace(),返回类型不尽相同。

set提供以下接口:

pair<iterator,bool> insert(const value_type& val);

iterator insert(const_iterator pos,const value_type& val);

template<typename...args>
pair<iterator,bool>emplace(Args&&...args);

multiset提供以下接口:

iterator insert(const value_type& val);

iterator insert(const_iterator pos,const value_type& val);

template<typename...args>
boolemplace(Args&&...args);

原因是:multiset允许元素重复,但是set不允许。如果往set中插入的元素set中已经有了的话,安插动作宣告失败。

几个注意事项:

  1. 面对以迭代器为元素的set,调用erase可能会造成歧义,为了这个因素,C++11提出修正,提供重载函数erase(iterator)和erase(const_iterator)。

  1. multiset的insert(),emplace()和erase()操作函数都会保留等价元素的相对次序。自C++11起,调用insert(val)和emplace(args...),新元素保证会安插于等价元素所形成的区间的末端

异常处理(Exception Handling)

面对多重元素安插动作,"保持元素次序"这一条件会造成"异常抛出时必须完全恢复原状变得不切实际"。因此,只有"单一元素安插动作"才支持"要么成功要么无效"的操作原则。

Set和Multiset运用实例

#incldue<iostream>
#incldue<set>
#incldue<algorithm>
#incldue<iterator>
using namespace std;

int main(){
    set<int,greater<int>>coll1;
    coll1.insert({4,3,5,1,6,2});
    coll1.insert(5);

    for(int elem:coll1){
        cout<<elem<<' ';
    }
    cout<<endl;

    auto status=coll1.insert(4);
    if(status.second){
         cout<<"4 inserted as element "
             <<distance(coll1.begin(),status.first)+1<<endl;
    }
    else cout<<"4 already exists"<<endl;

    //将coll1其中的元素拷贝给新的排序新标准coll2容器
    set<int>coll2(coll1.cbegin(),coll1.cend());
    copy(coll2.cbegin(),coll2.cend(),ostream_iterator<int>(cout," ");
    cout<<endl;

    coll2.erase(coll2.begin(),coll2.find(3));

    int num;
    num=coll2.erase(5);
    cout<<num<<" element(s) removed"<<endl;

    copy(coll2.cbegin(),coll2.cend(),ostream_iterator<int>(cout," ");
    cout<<endl;
}

输出结果:

6 5 4 3 2 1
4 already exists
1 2 3 4 5 6
1 element(s) removed
3 4 6

如果是multiset那么输出结果会发生一点改变:

6 5 4 3 2 1
4 already exists
1 2 3 4 5 6
1 element(s) removed
3 4 6
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Reol520

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值