c++编程(22)——STL(6)set

欢迎来到博主的专栏——c++编程
博主ID:代码小豪


文章目录

    • set
      • constructor
      • operator=
      • set的迭代器
      • set的增删查改

STL中的容器,其实就是将最常用的数据结构封装起来,让c++使用者可以实现功能时不必重复的“造轮子”(其实就是不用再写相关的类了)。

根据数据结构的特性,STL将容器分为序列式容器和关联式容器,序列式容器即:vector,list,deque以及由此衍生出来的适配器。

关联式容器有set和map两大类,其底层为RBtree,即红黑树,关于红黑树的数据结构底层讲解,博主将其放在数据结构专栏当中,本篇更注重于讲解STL中容器的使用方法以及细节。

set

关联式容器的模板类型与序列式容器有很大的不同,通常来说,序列式容器的模板类型参数只有两个,一个是决定数据类型的value_type,还有一个是分配器Alloc。

以set为例,其模板类型参数有三个(map则是有四个)

template < class T,                        // set::key_type/value_type
           class Compare = less<T>,        // set::key_compare/value_compare
           class Alloc = allocator<T>      // set::allocator_type
           > class set;

分别是决定数据类型的T,仿函数comp,分配器Alloc,关于仿函数comp起到的作用会在下文进行讲解。

请记住下列的数据类型别名,将会在set的成员函数中经常出现这些别名

别名 定义
value_type 模板参数T
key_type 模板参数T
key_compare 模板参数Compaare
value_compare 模板参数Compare
reference T&
pointer T*

与序列式容器不同。序列式容器主要的特性就是元素插入的顺序,比如尾插的元素排在后面,头插的元素排在前面,而关联式容器则不同,数据在容器中排列的位置与插入顺序无关,只与容器的性质相关,以set为例,set容器的主要作用是对数据进行排序和去重,因此不管用何种顺序插入完全相同的数据,这些数据在set当中的位置都是一样的。

set<int>s1{5,5,7,6,9,2,8,4};//2 4 5 6 7 8 9
set<int>s2{4,4,9,6,5,8,2,7};//2 4 5 6 7 8 9

由于set不允许数据重复,因此多次插入5和4只会保存第一个插入的5.可以发现,尽管s1和s2插入数据的顺序不同,但是数据在容器中的位置是一样的(至少迭代器迭代的顺序是一致的)。

观察上面数据,可以发现set容器中的数据都是升序排列的,难道只能升序排列吗?

set中的元素排列顺序与模板参数comp有关,这个comp实际上是一个仿函数参数,在默认情况下,set使用的是仿函数less,数据排列成升序;如果想要set的数据排列成降序,就使用库中的仿函数greater。(如果对仿函数不了解,请记住这个结论,博主将会在不久的将来填坑)

set<int,greater<int>>s3{ 5,5,7,6,9,2,8,4 };//9 8 7 6 5 4 2

constructor

set的构造函数分为5中(虽然下面只显示了四种,因为move构造暂时不打算讲)

empty构造

empty (1)	
explicit set (const key_compare& comp = key_compare(),
              const allocator_type& alloc = allocator_type());
explicit set (const allocator_type& alloc);

首先是set的empty构造,其作用是构造出一个没有元素的set容器。常用的方法有:

set<int>s1;

这种方法可以构造一个空的set容器,如果我们不想使用库中空间配置器,可以在参数中传入另一个空间配置器,当然基本不会这么做,因为很少有人能写出比STL中更好的空间配置器。通常情况下都是使用默认的。
range构造

range (2)	
template <class InputIterator>
  set (InputIterator first, InputIterator last,
       const key_compare& comp = key_compare(),
       const allocator_type& = allocator_type());

range构造允许我们使用一段迭代器区间[first,last)的数据来构造set容器。

vector<int> v1{ 5,4,7,9,6,3,8 };
set<int>s1(v1.begin(), v1.end());//相当于用{5,4,7,9,6,3,8}等元素构造了一个set容器

copy构造

copy (3)	
set (const set& x);
set (const set& x, const allocator_type& alloc);

set的拷贝构造可以复制另外一个set容器,由于set容器的底层是一颗红黑树,因此set的拷贝构造会走一个红黑树的深拷贝,其树状结构会与拷贝的set容器一致。(关于红黑树会在数据结构专栏中提到)。

set<int>s1{5,5,7,6,9,2,8,4};//2 4 5 6 7 8 9
set<int>s5 = s1;//s5拷贝s1
for (auto e : s5)
	cout << e << ' ';//2 4 5 6 7 8 9
cout<<endl;

initializer_list构造

initializer list (5)	
set (initializer_list<value_type> il,
     const key_compare& comp = key_compare(),
     const allocator_type& alloc = allocator_type());

initializer_list是c++11新增的类,其主要作用是允许用户使用初始化列表来初始化对象(关于initializer_list在c++杂谈专栏中关于c++11特性的文章中提到)。

初始化列表即**“{}”**,在列表中的数据会插入在set容器当中,set容器会对列表中的数据进行去重和排序的操作。

set<int>s6{ 6,8,2,5,3,5,5,7 };//去重和排序后变成2 3 5 6 7 8

operator=

copy (1)	
set& operator= (const set& x);
initializer list (3)	
set& operator= (initializer_list<value_type> il);

set的赋值重载函数可以为set容器拷贝其他容器的值。

set<int>s1{ 6,8,2,5,3,5,5,7 };//2 3 5 6 7 8
set<int>s2;
s2 = s1;//2 3 5 6 7 8

第二种使用方式使用initializer_list赋值给set容器

s3 = { 6,8,2,5,3,5,5,7 };//2 3 5 6 7 8

s3同样会对initializer_list的数据进行去重和排序操作

set的迭代器

set的迭代器是双向迭代器(bidirectional iterator),即允许迭代器进行++(访问下一个元素),–(访问上一个元素)的操作。

set的底层是一个红黑树,其迭代器迭代区间符合红黑树中序遍历的顺序。即迭代区间的起始指向中序遍历红黑树的第一个结果,由于c++规定,迭代区间必须是[begin,end),这意味着迭代区间的末尾并非指向中序遍历的最后一个结果。而是迭代区间的尾的上一个元素是中序遍历的最后一个结果。

begin()

iterator begin() noexcept;
const_iterator begin() const noexcept;

begin()函数返回指向set容器的首元素的迭代器(即迭代器的头),由于set容器会对数据进行排序,因此set容器的首元素则是容器存储数据中的最小值或最大值(取决于排序的规则)。

set<int>s1{ 5,9,6,7,8 };//5,6,7,8,9
cout<<*(s1.begin());//5

end()

iterator end() noexcept;
const_iterator end() const noexcept;

end()函数返回指向set容器迭代区间的末尾的迭代器,由于迭代区间的末尾不存在任何元素,因此该迭代器不指向有效数据,也无法引用。但是通过让末尾的迭代器访问上一个元素(--),可以访问set容器的最后一个有效数据,由于set容器会对数据进行排序,所以取到的是整个容器中的最大值或最小值(根据排序规则)。

set<int>s2{ 5,9,6,7,8 };//5,6,7,8,9
cout << *(s1.end());//error,end不指向有效的数据
cout << *(--s1.end());//9

set的迭代方式
set容器的迭代区间是[begin,end),而且set的迭代器通过自加(++),自减(--)的操作访问下一个\上一个元素,这意味着允许我们使用一个迭代器从头遍历整个set容器。

set<int>s1{ 5,9,6,7,8 };//5,6,7,8,9
set<int>::iterator begin = s1.begin();
while (begin != s1.end())
{
	cout << *begin << ' ';
	begin++;
}

如上,我们用迭代器begin来指向s1的起点,然后通过解引用得到对应的数据,在++使其指向下一个元素,直到begin指向迭代区间的末尾位置结束,通过这种方式可以遍历整个set容器。

rbegin和rend

reverse_iterator rbegin() noexcept;
const_reverse_iterator rbegin() const noexcept;
reverse_iterator rend() noexcept;
const_reverse_iterator rend() const noexcept;

rbegin和rend可以获得set的反向迭代区间的头和尾。所谓反向迭代区间,即正向迭代区间[begin,end)的倒置,即rbegin指向end的前一个元素,rend指向begin本身,且反向迭代器++是访问上一个元素,--是访问下一个元素。

set<int>s1{ 5,9,6,7,8 };//5,6,7,8,9
set<int>::reverse_iterator rbegin = s1.rbegin();
while (rbegin != s1.rend())
{
	cout << *rbegin << ' ';//9 8 7 6 5
	rbegin++;
}

set的增删查改

insert
set的插入不再是单纯的头插尾插或是任意位置插入,而是只有插入,关于set的插入原理可以参考红黑树的插入操作,如果从表象上理解就是向set容器插入一个数据,这个数据不会破坏容器的有序。

single element (1)	
pair<iterator,bool> insert (const value_type& val);
with hint (2)	
iterator insert (const_iterator position, const value_type& val);
range (3)	
template <class InputIterator>
  void insert (InputIterator first, InputIterator last);
initializer list (4)	
void insert (initializer_list<value_type> il);

由于set中的元素是唯一的,插入操作检查插入的每个元素是否与容器中已有的元素相等,如果相等,则不插入该元素,并返回一个指向该现有元素的迭代器。

(1)(2)都是单元素插入,区别在于(2)可以指定插入位置(并非强制插入,而是提醒该位置可能符合set有序的特性,如果不满足,是不会在这个位置插入元素的)。通常来说,(2)有点冗余了,基本上都是用(1)法。

set<int>s1{ 5,9,6,7,8 };//5,6,7,8,9
s1.insert(10);//5 6 7 8 9 10

insert的返回值是一个pair类型,从底层上说则是一个包含两个成员对象的结构体,第一个成员叫first,第二个成员叫second。insert的返回值则是包含一个迭代器,和布尔值的pair结构体,即firrst指向插入的位置,second则说明插入操作有没有成功。

(3)和(4)则是多个值进行插入,关于迭代区间或者初始化列表的插入可以参考range构造(上文)。简单来说就是将区间内的所有值作为插入的元素。

vector<int>v1{ 1,2,3,4 };
set<int>s1{ 5,9,6,7,8 };//5,6,7,8,9
s1.insert(v1.begin(), v1.end());//1 2 3 4 5 6 7 8 9

亦或者

set<int>s1{ 5,9,6,7,8 };//5,6,7,8,9
s1.insert({ 1,2,3,4 });//1 2 3 4 5 6 7 8 9

erase

(1)	
iterator  erase (const_iterator position);
(2)	
size_type erase (const value_type& val);
(3)	
iterator  erase (const_iterator first, const_iterator last);

(1)定点删除元素
传入待删除元素的迭代器,定点删除该元素

set<int>s1{ 5,9,6,7,8 };//5,6,7,8,9
s1.erase(s1.begin());//6,7,8,9

(2)删除对应key值
传入要删除的key值,函数会在树中找到对应节点进行删除

set<int>s1{ 9,6,7,8 };//6,7,8,9
s1.erase(7);//6 8 9

(3)删除迭代区间内的元素
传入待删除的迭代区间,set容器会删除区间内的所有元素

set<int>s2{ 5,9,3,4,7,8,6 };//3 4 5 6 7 8 9
s2.erase(++s2.begin(), --s2.end());//3 9

swap

void swap (set& x);

swap可以使两个set容器之间的所有数据进行交换

set<int>s1{6,8,9};
set<int>s2{3,9};
s1.swap(s2);//s1:3 9 s2:6 8 9

clear

void clear() noexcept;

删除容器内的所有元素。

find

const_iterator find (const value_type& val) const;
iterator       find (const value_type& val);

在set容器中查找与val等效的元素,如果找到则返回该元素的迭代器,否则返回set::end。

  set<int> myset;
  set<int>::iterator it;

 for (int i=1; i<=5; i++) myset.insert(i*10);    // set: 10 20 30 40 50

  it=myset.find(20);
  myset.erase (it);//删除20对应的元素
  myset.erase (myset.find(40));//删除40对应的元素

在上述代码中,it通过find函数找到20对应的元素,获得了指向该元素的迭代器,又通过迭代器删除了这个元素,同理,先是通过find函数获得了40元素对应的迭代器,然后传入erase函数中删除该元素

你可能感兴趣的:(c++编程,c++,开发语言)