stl容器包括序列容器,关联式容器
所谓序列容器,即以线性排列(类似普通数组的存储方式)来存储某一指定类型(例如 int、double 等)的数据,需要特殊说明的是,该类容器并不会自动对存储的元素按照值的大小进行排序。
需要注意的是,序列容器只是一类容器的统称,并不指具体的某个容器,序列容器大致包含以下几类容器:
- array<T,N>(数组容器):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值;
- vector<T>(向量容器):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数);
- deque<T>(双端队列容器):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;
- list<T>(链表容器):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list<T> 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
- forward_list<T>(正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。
vector是c++ 最常用的容器之一,下面介绍一下一些比较常见的用法。
vector是一个有序容器,下面总结一下vector的用法,主要是初始化、增、删、改、查、迭代器六个方面
初始化
std::vector<int> vec1; // 初始化一个空vector
std::vector<int> vec2(3); // 初始化一个长度为3的vector,会赋予默认值,但是不建议用默认值。
std::vector<int> vec3(3,5); // 初始化一个长度是3,每个值都是5的vector
std::vector<int> vec4 = {1,2,3,4,5,6,7}; // 拷贝初始化,直接定义vector的值
std::vector<int> vec4_{1,2,3,4,5,6,7}; // 用法与上面相同
std::vector<int> vec5(vec4); // 把vec4的值拷贝给vec5
std::vector<int> vec6(vec4.begin(), vec4.begin() + 3); // 把vec4的前3个值初始化给vec6
std::vector<int> vec7 = {vec4.begin(), vec4.begin() + 3}; // 用法与上相同
std::cout << "vec2 " << vec2[1] << std::endl; // 默认值是0
std::cout << "vec3 " << vec3[1] << std::endl;
std::cout << vec4[2] << std::endl;
std::cout << vec4_[2] << std::endl;
std::cout << "vec6 " << vec6.size() << std::endl; // vec6的尺寸是3
std::cout << "vec7 " << vec7.size() << std::endl;
查:
包括查询vector的大小,查询vector的容量,vector的某个值,遍历vector:
std::vector<int> vec = {1,2,3,4,5};
std::cout << "vec " << vec.size() <<std::endl; // vector 大小
std::cout << "vec " << vec.empty() << std::endl; // vector 是否为空
std::cout << "vec " << vec.capacity() << std::endl; /// vector容量,超过该容量则需要重新分配内存
std::cout << "vec " << vec[2] << std::endl; // 获取某个元素
std::cout << "vec " << vec.at(2) << std::endl; // 获取某个元素
std::cout << vec.front() << std::endl; //获取第一个元素
cout << vec.back() << endl; 获取最后一个元素
for(auto i: vec){
std::cout << "vec1 " << i << std::endl; // 获取某个元素
}
for(int i = 0; i < vec.size();i++){
std::cout << "vec2 " << vec[i] << std::endl; // 获取某个元素
}
for(auto i = vec.begin(); i < vec.end();i++){
std::cout << "vec3 " << *i << std::endl; // 获取某个元素
}
增
implace_back: vector最后添加元素
std::vector<int> vec = {1,2,3,4,5};
for(auto i: vec){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
vec.emplace_back(100); // 在vector后面增加一个元素
for(auto i: vec){
std::cout << i << " "; // 获取某个元素
}
insert
insert的用法非常灵活,本质上是对指针的处理。他有两种用法
insert(pos, value) 其中value是一个值,那么会把该值插入到pos的位置上,例如:
std::vector<int> vec = {1,2,3,4,5};
for(auto i: vec){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
// 输出是1,2,3,4,5
vec.insert(vec.begin(),100);
for(auto i: vec){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
// 输出是1,2,3,4,5,100
insert(pos, address_begin, address_end) 即把某一段内存地址中的数据数据插入到指定位置。例如:
std::vector<int> vec = {1,2,3,4,5};
for(auto i: vec){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
std::vector<int>inserted_vec = {101,102,103};
vec.insert(vec.begin() + 1,inserted_vec.begin(),inserted_vec.begin()+1);
for(auto i: vec){
std::cout << i << " "; // 获取某个元素
}
//输出 1 101 2 3 4 5
为什么他的用法会有两种用法呢
这里要说到insert的本质了,insert并不关心被插入的一段内存地址到底是不是vector,就算是其他数据格式,比如普通数组,insert也照样能插入。即如下代码能够正常运行:
std::vector<int> vec = {1,2,3,4,5};
for(auto i: vec){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
int inserted_array []= {101,102,103};
vec.insert(vec.begin() + 1,inserted_array,inserted_array+3);
for(auto i: vec){
std::cout << i << " "; // 获取某个元素
}
// 输出 1 101 102 103 2 3 4 5
我们再回过头来看第一种用法,既然insert插入的是内存地址,那么是不是第一种用法只是第二种用法的特例呢?
vec.insert(vec.begin(),100);
这种用法的本质是
vec.insert(vec.begin(),{100});
而这种用法等于把100的看做一段内存
如果我们这样用,会更容易理解一些:
vec.insert(vec.begin(),{100, 99, 98});
相当于在内存中开辟了一个三个int,分别存放100,99,98。然后把收尾的地址给了insert。
所以第一种用法和第二种用法的本质是一样的。
insert的用法非常灵活,所以insert可以应付绝大部分vector的插入场景。
例如在vector最后插入一个元素可以这样写:
std::vector<int> vec = {1,2,3,4,5};
vec.insert(vec.end(), 20);
合并两个vector:
std::vector<int> vec1 = {1,2,3,4};
std::vector<int> vec2(3,5);
vec1.insert(vec1.end(), vec2.begin(), vec2.end());
for(auto i: vec1){
std::cout << i << " ";
}
std::cout << std::endl;
应该注意的一点是,insert插入的时候,终点未知的数据并不插入到vector中,例如
vec1.insert(vec1.end(), vec2.begin(), vec2.begin()+1);
vec2.begin()+1 的元素不会插入到vector中
删:
clear 删除vector中所有元素
std::vector<int> vec = {1,2,3,4,5};
vec.clear();
// 删除了vec的所有元素,
clear删除以后,vect的size是0 ,但是capacity没有发生变化。有一个现象需要注意。
std::vector<int> vec = {1,2,3,4,5};
vec.clear();
std::cout << "size " << vec.size() << " " << vec[2] << std::endl;
// 出输出 size 0 3
上面代码不会报错的原因是clear以后,vec的end会前移到vec begin的位置,但是原本放在内存中的数据并不会消失,所以还是能取到。所以clear是一个变更vec end的操作。
pop_back 删除最后一个元素。
std::vector<int> vec = {1,2,3,4,5};
vec.pop_back();
for(auto i: vec){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
// 输出1 2 3 4
同样的,pop_back只是移动了end的位置,并没有对内存中的数据做操作,下面的代码可以说明这个现象。虽然已经删除了元素5,但是仍然可以取到。
std::vector<int> vec = {1,2,3,4,5};
vec.pop_back();
std::cout << "forth " << vec[4] << std::endl;; // 获取某个元素
erase 删除vector中的某一段数据。
std::vector<int> vec = {1,2,3,4,5};
vec.erase(vec.begin()+1,vec.begin()+3);
for(int i = 0; i <vec.size(); i ++){
std::cout << vec[i] << " "; // 获取某个元素
}
// 输出1 2 3 4
std::cout << std::endl;
std::cout << "forth " << vec[3] << std::endl;; // 获取某个元素
输出4
可以看到,erase的删除过程是把后面的元素一次向复制一次。所以erase不仅修改end的位置,也修改各个位置上的元素值。
这是因为pop_back和erase操作都不会影响vector的capacity,只会影响size
std::vector<int> vec = {1,2,3,4,5};
std::cout << "before erase " << vec.size() << " " << vec.capacity() << std::endl;
vec.erase(vec.begin()+1,vec.begin()+3);
std::cout << "after erase " << vec.size() << " " << vec.capacity() << std::endl;
// 输出
// before erase 5 5
// after erase 3 5
改:
直接修改某个元素的值:
std::vector<int> vec = {1,2,3,4,5};
vec[3] = 100;
assign函数:
assign函数有三种用法:
vec.assign( int, value) : 意思是重新给vec赋值成为int个value。例如
std::vector<int> vec = {1,2,3,4,5};
for(int i = 0; i <vec.size(); i ++){
std::cout << vec[i] << " "; // 获取某个元素
}
std::cout << std::endl;
//输出 1 2 3 4 5
vec.assign(3,100);
for(int i = 0; i <vec.size(); i ++){
std::cout << vec[i] << " "; // 获取某个元素
}
std::cout << std::endl;
// 输出100 100 100
vec.assign( vec2.begin(), vec2.end() )
std::vector<int> vec = {1,2,3,4,5};
for(int i = 0; i <vec.size(); i ++){
std::cout << vec[i] << " "; // 获取某个元素
}
std::cout << std::endl;
// 输出1 2 3 4 5
std::vector<int> vec2(3,5);
vec.assign(vec2.begin(), vec2.end());
for(int i = 0; i <vec.size(); i ++){
std::cout << vec[i] << " "; // 获取某个元素
}
// 输出5 5 5
需要留意的地方是string类型也可以用assign类型进行赋值,下面粘一段c++官方的代码:
#include <vector>
#include <iostream>
#include <string>
int main()
{
std::vector<char> characters;
auto print_vector = [&](){
for (char c : characters)
std::cout << c << ' ';
std::cout << '\n';
};
characters.assign(5, 'a');
print_vector();
const std::string extra(6, 'b');
characters.assign(extra.begin(), extra.end());
print_vector();
characters.assign({'C', '+', '+', '1', '1'});
print_vector();
}
这里有一个lambda函数,具体可以看 c++ lambda和匿名函数
swap
swap函数用于把两个vector的元素互换,例如
std::vector<int> vec1 = {1,2,3,4};
std::vector<int> vec2(7,5);
std::cout << "互换之前 vec1:" ;
for(auto i: vec1){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
std::cout << "互换之前 vec2:" ;
for(auto i: vec2){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
vec1.swap(vec2);
std::cout << "互换之后 vec1:" ;
for(auto i: vec1){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
std::cout << "互换之后 vec2:" ;
for(auto i: vec2){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
// 互换之前 vec1:1 2 3 4
// 互换之前 vec2:5 5 5 5 5 5 5
// 互换之后 vec1:5 5 5 5 5 5 5
// 互换之后 vec2:1 2 3 4
swap有一个非常重要的作用就是清空内存,因为前面已经提到了clear和erase都不会把已经占用的内存清理掉,vector的capacity只会越来越大,这些内存会持续被占用直到vector被解构。但是swap的过程中,等于是解构了原来的vector,能够清理被过多使用的内存。
reserve && resize 都是修改vector的容量,但是有所区别
reserve: 分配空间,更改capacity但不改变size
resize: 分配空间,更改capacity也改变size
std::vector<int> vec1 = {1,2,3,4};
std::vector<int> vec2(vec1);
std::cout << "调整之前 vec1 capacity " << vec1.capacity() << " vec1 size " << vec1.size() << std::endl;
std::cout << "调整之前 vec2 capacity " << vec2.capacity() << " vec2 size " << vec2.size() << std::endl;
vec1.reserve(20);
vec2.resize(20);
std::cout << "调整之后 vec1 capacity " << vec1.capacity() << " vec1 size " << vec1.size() << std::endl;
std::cout << "调整之后 vec2 capacity " << vec2.capacity() << " vec2 size " << vec2.size() << std::endl;
// 输出
调整之前 vec1 capacity 4 vec1 size 4
调整之前 vec2 capacity 4 vec2 size 4
调整之后 vec1 capacity 20 vec1 size 4
调整之后 vec2 capacity 20 vec2 size 20
reverse 反转容器中一段数据
#include <algorithm>
std::vector<int> vec1 = {1,2,3,4};
std::cout << "反转之前 ";
for(auto i: vec1){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
std::reverse(vec1.begin(), vec1.begin()+2);
std::cout << "反转之后 ";
for(auto i: vec1){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
输出
反转之前 1 2 3 4
反转之后 2 1 3 4
sort,对容器中的一段数据排序,默认是升序
std::vector<int> vec = {4,1,5,3,5,2,6,7,2,4,1,5};
std::cout << "排序之前: " ;
for(auto i: vec){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
std::sort(vec.begin(), vec.begin()+5);
std::cout << "排序之后: " ;
for(auto i: vec){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
输出
排序之前: 4 1 5 3 5 2 6 7 2 4 1 5
排序之后: 1 3 4 5 5 2 6 7 2 4 1 5
在sort中不建议使用rbegin和rend,如果使用则正确的写法是
std::sort(vec.rbegin(), vec.rbegin()+5);
这样写虽然不报错,但是会降序排序
迭代器
迭代器是用来访问容器的入口,因为不是所有的容器都可以通过下表得到元素,而所有的容器都可以通过迭代器获得元素。迭代器类似于指针,通过++ 操作可以使指针移动到“下一个”(为什么是引号的下一个,下面会有解释)元素的位置,无论这个容器的底层存储格式是数组还是指针。
迭代器不仅用于vector几乎可与用于所有的容器中
迭代器一个最典型的用法:
std::vector<int> vec = {1,2,3,4,5};
std::vector<int>::iterator iter;
for(iter = vec.begin();iter < vec.end(); iter++){
std::cout << *iter << " ";
}
// 输出1 2 3 4
可以看到的是begin(), end() 函数都会返回一个迭代器,此外还有一些常见的用法:
begin() | 返回指向容器中第一个元素的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。 |
end() | 返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。 |
rbegin() | 返回指向最后一个元素的反向迭代器;如果是 const 类型容器,在该函数返回的是常量反向迭代器。 |
rend() | 返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,在该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。 |
cbegin() | 和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。 |
需要注意的是end() 指向的是容器最后一个元素的后一个位置,而rbegin()指向的是最后一个元素,这2个位置并不重合,用一张图表示就是
逆向迭代器的使用
rbegin和rend返回结果是逆向迭代器,所以迭代器的“下一个” 实际上是正常迭代器的 上一个,所以++的操作符会使迭代器指向上一个元素。这也是为什么前面的 “下一个”会带一个引号。
实际上rbegin ,rend 是一个反向迭代器,和正常的迭代器并不是同一个对象,下面的代码能够很直观的表现出来
std::vector<int> vec = {4,1,5,3,5,2,6,7,2,4,1,5};
std::vector<int>:: iterator iter1; // 报错
std::vector<int>:: reverse_iterator iter1; // 正确
for(iter1 = vec.rbegin(); iter1 < vec.rend(); iter1 ++) {
std::cout << *iter1 << " ";
}
很多以迭代器为输入的函数,如果想把逆向迭代器当做这些函数的入参的时候要格外的小心和注意,有很多不支持逆向迭代器,即便支持,返回的结果可能和预期的结果也不尽相同。
erase : 不支持逆向迭代器
insert:支持,但是会逆序插入
std::vector<int> vec1 = {1,2,3,4};
std::vector<int> vec2(vec1);
vec1.insert(vec1.end(), vec2.rbegin(), vec2.rbegin()+2);
for(auto i: vec1){
std::cout << i << " ";
}
std::cout << std::endl;
输出
1 2 3 4 4 3
reverse : 支持,结果和正常迭代器一致
std::vector<int> vec1 = {1,2,3,4};
std::cout << "反转之前 ";
for(auto i: vec1){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
std::reverse(vec1.rbegin(), vec1.rbegin()+3);
std::cout << "反转之后 ";
for(auto i: vec1){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
输出
反转之前 1 2 3 4
反转之后 1 4 3 2
assign 支持 逆序赋值
std::vector<int> vec1 = {1,2,3,4};
std::vector<int> vec2(vec1);
for(auto i: vec2){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
vec2.assign(vec1.rbegin(), vec1.rbegin()+3);
for(auto i: vec2){
std::cout << i << " "; // 获取某个元素
}
std::cout << std::endl;
输出
1 2 3 4
4 3 2
初始化 支持,逆序初始化
std::vector<int> vec1 = {1,2,3,4};
std::vector<int> vec2 = {vec1.rbegin(), vec1.rbegin() + 3};
for(auto i: vec2){
std::cout << i << " ";
}
std::cout << std::endl;
输出
4 3 2