欢迎来到博主的专栏——c++编程
博主ID:代码小豪
根据数据结构的特性,STL将容器分为序列式容器和关联式容器,序列式容器即:vector,list,deque以及由此衍生出来的适配器。
关联式容器有set和map两大类,其底层为RBtree,即红黑树,关于红黑树的数据结构底层讲解,博主将其放在数据结构专栏当中,本篇更注重于讲解STL中容器的使用方法以及细节。
关联式容器的模板类型与序列式容器有很大的不同,通常来说,序列式容器的模板类型参数只有两个,一个是决定数据类型的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<int,greater<int>>s3{ 5,5,7,6,9,2,8,4 };//9 8 7 6 5 4 2
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
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的迭代器是双向迭代器(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++;
}
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函数中删除该元素