文章目录
☛ 标准模板库STL框架
点开看更清楚🧐
☛ 顺序容器都有的函数
(一)构造函数
系统为顺序容器提供了4种构造方式,即定义方式,以vector为例:
- 默认构造: 直接开辟空间;如vector<int> vec
- 带有一个整型n的构造:开辟n个大小的空间,用0初始化;如vector<int> vec(10),开辟10个空间,初始化为0
- 带有2个参数n,m的构造函数 :开辟n个空间,用m初始化。如vector<int>vec(10,20),开辟10个空间,用20初始化。
- 传入迭代器(指针)区间的构造函数:传入起始位置,末尾位置,会把起始~末尾的迭代器区间元素传入容器。注意也可以传入数组,字符串的指针。如vector<int> vec(arr,arr+len),将arr~arr+len区间的数据传入容器。
list,deque同理,都有这四种构造方式。
(二)函数
格式 | 含义 |
---|---|
c.begin(); | 返回一个迭代器,它指向容器c的第一个元素。 |
c.end(); | 返回一个迭代器,它指向容器c的最后一个元素的下一位置,是一个无效位置。 |
c.rbegin(); | 返回一个逆迭代器,它指向容器c的最后的一个元素。 |
c.rend(); | 返回一个逆迭代器,它指向容器c的第一个元素前面的位置,是一个无效位置。 |
c.size(); | 返回容器c中的元素个数。 |
c.max_size(); | 返回容器c可容纳的最多元素个数。 |
c.empty(); | 返回容器大小是否为0的布尔值。 |
c.clear(); | 删除容器c内的所有元素。返回void。 |
(三)遍历方式
迭代器(后面会学到)是面向对象的指针,和普通指针类似,可以通过迭代器来遍历容器中的元素。 以vector为例,list,deque同理:
vector<int> it = vec.begin();
for (; it != vec.end(); ++it)
{
cout << *it << " ";
}
一、vector
vector底层是一个数组,使用时需要添加头文件:
# include<vector>
(一)对vector容器的操作
1. 增
vector是矢量容器,一端固定,向另一端增长,所以头部固定,不允许头插。支持尾插,任意位置插入:
插入格式 | 形式 | 含义 | 时间复杂度 |
---|---|---|---|
尾插 | push_back(a) | 将数据a尾插到容器中 | 尾插没有数据移动,所以时间复杂度为O(1) |
任意位置插入 | insert(index,first,last) | 将first~last区间内的元素插入index位置 | 存在数据的移动,时间复杂度为O(n) |
任意位置插入 | insert(index,val) | 将val元素插入index位置 | 存在数据的移动,时间复杂度为O(n) |
任意位置插入 | insert(index,count,val) | 在index位置插入count个val元素 | 存在数据的移动,时间复杂度为O(n) |
注意:插入时的位置,需要用迭代器表示,不能用下标表示,即:
std::vector<int>::iterator it=vec.begin();
vec.insert(it,a,a+len);//正确
vec.insert(2,a,a+len)/失败,不能将2转换为迭代器类型
2. 删
同理没有头删,只有尾删和任意位置删除,如下:
删除格式 | 形式 | 含义 | 时间复杂度 |
---|---|---|---|
尾删 | pop_back() | 将容器最后一个元素删除 | 尾删没有数据移动,所以时间复杂度为O(1) |
任意位置删除 | erase(first,last) | 删除first~last区间的元素 | 存在数据的移动,时间复杂度为O(n) |
任意位置删除 | erase(it) | 删除it位置的元素 | 存在数据的移动,时间复杂度为O(n) |
3. 访问
根据vector的特性:vector底层是数组,故可以根据下标进行随机访问,没有数据的移动,时间复杂度为O(1)。
遍历容器:
- 根据下标
- 根据迭代器,上面已经阐述。
方法 | 格式 |
---|---|
利用下标 | for循环从下标0~size()进行元素的输出 |
利用迭代器 | while循环,从vec.begin()开始位置,遍历到vec.end()位置 |
注意:容器都可以使用迭代器遍历,list,deque同理。
4. 其他操作
格式 | 含义 |
---|---|
reserve(n) | 给vector预留n个空间,给容器底层开辟指定大小的内存空间,并不会添加新的元素 |
resize(n) | 容器扩容为n个空间,给容器底层开辟指定大小的内存空间,用0初始化 |
swap | 两个容器进行元素交换 |
(二)扩容机制
动态开辟内存,vector在内存足够情况下,可以无限存储元素,故vector存在扩容机制,调用函数resize(n),扩容流程:
- 先开辟一定大小,
- 以倍数的形式开辟更大的空间,通常以2倍开辟
- 把旧的数据拷贝到新空间中
- 释放旧空间
- 指针指向新空间,调整总大小。
(三)特点
根据vector的操作时间复杂度,我们可以总结出vector的优缺点:
操作 | 时间复杂度 |
---|---|
push_back | O(1) |
insert | O(n) |
pop_back() | O(1) |
erase() | O(n) |
访问 | O(1) |
时间复杂度为O(1)就是优点,O(n)就是缺点:
- 优点:支持尾部快速的插入或删除,直接访问任何元素。
- 缺点:按位置删除,插入速度慢。
(四)演示
对上面的操作进行一个演示:
# include<iostream>
# include<vector>
template <typename T>
void show( std::vector<T>& vec)
{
std::vector<T>::iterator it=vec.begin();
while(it!=vec.end())
{
std::cout<<*it<<" ";
it++;
}
std::cout<<std::endl;
}
int main()
{
std::vector<int> ivec;//无参构造
std::vector<double> dvec(10);//一个int参数的构造
std::vector<char> cvec(10,'c');//两个int参数的构造
int a[]={0,1,2,3,4,5,6,7,8,9,10,11,12};
int len=sizeof(a)/sizeof(a[0]);
std::vector<int> vec(a,a+len);//通过迭代器区间构造
//1.数组打印字符迭代器
for(int i=0;i< cvec.size();i++)
{
std::cout<<cvec[i]<<" ";
}
std::cout<<std::endl;
//2.迭代器打印整型迭代器
show<int>(vec);
//3.尾插10
vec.push_back(10);
std::cout<<"尾插10"<<std::endl;
show<int>(vec);
//4.迭代器指向起始位置,将数组0~5的元素插入2号下标
std::vector<int>::iterator it=vec.begin();
vec.insert(it+2,a,a+5);
show<int>(vec);
//5.在0号下标插入2个100
vec.insert(it,2,100);//插入过后,迭代器指向的位置变了,迭代器失效,所以下面使用begin()标识位置
show<int>(vec);
//6.删除前0~9之间的元素元素
vec.erase(vec.begin(),vec.begin()+10);//删除0~9
show<int>(vec);
//5.删除第2个元素
vec.erase(vec.begin()+2);
show<int>(vec);
//6.删除尾部元素
vec.pop_back();
show<int>(vec);
//7.扩容,扩大空间,并初始化为0
std::cout<<"原来大小:"<<vec.size()<<std::endl;
vec.resize(20);//扩容到20个大小
std::cout<<"现在大小:"<<vec.size()<<std::endl;
show<int>(vec);
//8.预留空间,不用就不会使用,原大小不变
std::cout<<"原来大小:"<<vec.size()<<std::endl;
vec.reserve(40);//预留40个大小
std::cout<<"现在大小:"<<vec.size()<<std::endl;
show<int>(vec);
//9.清除
vec.clear();
show<int>(vec);
}
运行结果为:
二、list
list底层是一个双向链表,是环状的,所以是双向循环链表:
使用时添加头文件:
# include<list>
(一)对list容器的操作
1. 增
双向循环链表,可以头插,尾插,任意位置插入,所以:
插入格式 | 形式 | 含义 | 时间复杂度 |
---|---|---|---|
头插 | push_front(a) | 将a元素插入到容器头部 | 没有数据的移动,时间复杂度为O(1) |
尾插 | push_back(a) | 将a元素插入容器尾部 | 没有数据的移动,时间复杂度为O(1) |
任意位置插入 | insert(index,first,last) | 将first~last区间内的元素插入index位置 | 链表通过指针指向,所以不存在数据的移动,时间复杂度为O(1) |
任意位置插入 | insert(index,val) | 将val元素插入index位置 | 不存在数据的移动,时间复杂度为O(1) |
任意位置插入 | insert(index,count,val) | 在index位置插入count个val元素 | 不存在数据的移动,时间复杂度为O(1) |
2. 删
三种删除方式:头删,尾删,任意位置删。
删除格式 | 形式 | 含义 | 时间复杂度 |
---|---|---|---|
头删 | pop_front() | 将容器第一个元素删除 | 头删没有数据移动,时间复杂度为O(1) |
尾删 | pop_back() | 将容器最后一个元素删除 | 有尾指针,所以尾删没有数据移动,所以时间复杂度为O(1) |
任意位置删除 | erase(first,last) | 删除first~last区间的元素 | 不存在数据的移动,时间复杂度为O(1) |
任意位置删除 | erase(it) | 删除it位置的元素 | 不存在数据的移动,时间复杂度为O(1) |
3. 访问
list是双向循环链表,由结点构成,所以不需要扩容,访问一个结点时,需要循环通过指针找到此结点,所以访问的时间复杂度为O(n)。
(二)特点
根据list的操作时间复杂度,我们可以总结出list的优缺点:
操作 | 时间复杂度 |
---|---|
push_front | O(1) |
push_back | O(1) |
insert | O(1) |
pop_front | O(1) |
pop_back() | O(1) |
erase() | O(1) |
访问 | O(n) |
- 优点:任意位置快速的插入和删除元素。
- 缺点:访问效率低
(三)list和vector的比较
- vector :底层是数组,数组内存空间连续,迭代器可以进行+i,-i的操作,是不会出错的,所以vector支持随机访问迭代器。
- list:底层是双向循环链表,内存不一定连续,故迭代器进行+i,-i操作时不一定能找到下一个结点,可能会发生内存越界的问题。所以list不支持随机访问迭代器,支持双向迭代器,只能提供++,–方式访问内存。
比较角度 | vector | lsit |
---|---|---|
底层结构 | 动态顺序表,一段连续的空间 | 带头结点的双向链表 |
随机访问 | 支持随机访问,访问时间复杂度O(1) | 不支持随机访问,访问时间复杂度O(n) |
插入删除 | 效率低,存在搬移元素,时间复杂度O(n)。插入还有可能造成扩容、增容、开辟空间、拷贝元素等等操作 | 任意位置插入和删除效率高,时间复杂度O(1),不需要增容扩容 |
空间利用率 | 因为底层连续空间,空间利用率高,内存碎片少 | 底层为链表结构,空间利用率低,内存碎片多 |
迭代器 | 随机访问迭代器 | 双向迭代器 |
迭代器失效 | 存在插入删除元素时导致的失效问题,当前迭代器需要重新赋值 | 因为只支持++,- -操作,所以没有迭代器失效问题 |
使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |
(四)演示
# include<iostream>
# include<list>
template <typename T>
void show( std::list<T>& vec)
{
std::list<T>::iterator it=vec.begin();
while(it!=vec.end())
{
std::cout<<*it<<" ";
it++;
}
std::cout<<std::endl;
}
int main()
{
std::list<int> ilis;//无参构造
std::list<double> dlis(10);//一个int参数的构造
std::list<char> clis(10,'c');//两个int参数的构造
int a[]={0,1,2,3,4,5,6,7,8,9,10,11,12};
int len=sizeof(a)/sizeof(a[0]);
std::list<int> lis(a,a+len);//通过迭代器区间构造
//2.迭代器打印整型迭代器
show<int>(lis);
//3.尾插10
lis.push_back(10);
std::cout<<"尾插10"<<std::endl;
show<int>(lis);
//头插200
lis.push_front(200);
std::cout<<"尾插200"<<std::endl;
show<int>(lis);
//4.迭代器指向起始位置,将数组0~5的元素插入0号下标
std::list<int>::iterator it=lis.begin();//支持双向迭代器,所以不能it+i,只能it++
lis.insert(it,a,a+5);
show<int>(lis);
//5.在0号下标插入2个100
lis.insert(lis.begin(),2,100);//插入过后,迭代器指向的位置变了,迭代器失效,所以下面使用begin()标识位置
show<int>(lis);
//5.删除第2个元素
lis.erase(lis.begin());
show<int>(lis);
//6.删除尾部元素
lis.pop_back();
show<int>(lis);
//删除头部元素
lis.pop_front();
show<int>(lis);
//7.扩容,扩大空间,并初始化为0
std::cout<<"原来大小:"<<lis.size()<<std::endl;
lis.resize(20);//扩容到20个大小
std::cout<<"现在大小:"<<lis.size()<<std::endl;
show<int>(lis);
//6.删除前开始~结尾之间的元素
lis.erase(lis.begin(),lis.end());//删除0~3
show<int>(lis);
}
三、deque
deque是双端队列容器,即两边都可以进行插入,删除;使用时使用一端受限的双端队列,需要添加头文件:
# include<deque>
【底层处理:】
分为两部分:
- 映射区域:指针数组,存储指针类型的数组
- 数据区域:new开辟的数组,大小固长,为512个字节
通过映射区映射到数据区域,此时的队头,队尾指针指向数据区域的中间位置,方便头插和尾插:
- 如果指向头部,那么头插就要开辟空间
- 指向尾部,尾插就要开辟空间
其实还有很多空间没有,导致空间利用率低,故指向中间。
deque做了特殊处理(如让front的下一个就是第二个数据块,这样使得数据块连续起来,类似于二维数组),使得内存在物理上不连续,逻辑上连续,即内存逻辑上连续,可以通过+i的方式访问,那么就可以支持随机访问迭代器。
(一)对deque容器的操作
1. 增
插入格式 | 形式 | 含义 | 时间复杂度 |
---|---|---|---|
头插 | push_front(a) | 将a元素插入到容器头部 | 有数据的移动,时间复杂度为O(1) |
尾插 | push_back(a) | 将a元素插入容器尾部 | 有数据的移动,时间复杂度为O(1) |
任意位置插入 | insert(index,first,last) | 将first~last区间内的元素插入index位置 | 存在数据的移动,时间复杂度为O(n) |
任意位置插入 | insert(index,val) | 将val元素插入index位置 | 存在数据的移动,时间复杂度为O(n) |
任意位置插入 | insert(index,count,val) | 在index位置插入count个val元素 | 存在数据的移动,时间复杂度为O(n) |
2. 删
三种删除方式:头删,尾删,任意位置删。
删除格式 | 形式 | 含义 | 时间复杂度 |
---|---|---|---|
头删 | pop_front() | 将容器第一个元素删除 | 头删没有数据移动,时间复杂度为O(1) |
尾删 | pop_back() | 将容器最后一个元素删除 | 有尾指针,所以尾删没有数据移动,所以时间复杂度为O(1) |
任意位置删除 | erase(first,last) | 删除first~last区间的元素 | 存在数据的移动,时间复杂度为O(n) |
任意位置插入 | erase(it) | 删除it位置的元素 | 存在数据的移动,时间复杂度为O(n) |
3. 访问
deque双端队列,类似于二维数组,保证了内存逻辑连续,可以通过下标进行访问,所以时间复杂度为O(1)。
(二)扩容机制
deque可以理解为二维数组,定义时只开辟了512字节大小的数据区域,当尾插或者头插到最后或开头时,就需要进行扩容。我们以头插为例说明deque的扩容机制。
当头插满了后,再进行头插,就需要进行扩容,如下图:
先看指向这块数据区域的映射区域的上方,是否有空间,没有看下方,有空间将数据区域进行下移动,移动到新的映射区,原来的映射区开辟一样大的内存区域,pfront指针向上走,ptail指针向下走。现在下方有空间,若以数据下移动,开辟新数据域,上方指向:
如果上下都没有空间,那就开始扩充映射区域,以倍数增长,扩充为两倍,系统会将原来的数据区域放到中间进行映射,头指针就在上方开辟数据域,上方映射区指向,尾指针相反,如下图:
数据域放到中间的目的: 这样上面,下面都有映射空间。下次扩容时,不管向上,还是向下移动,都可以直接开辟出来数据空间,让上/下映射区指向即可。
总结扩容方法:
【1. 头部指针需要扩容----头指针向上移动】
- 先看当前映射区的上面是否有空闲映射区,有,开辟一样大的数据区域,让上方映射区指向。
- 如果上方没有,看下方是否有映射区域,有,将数据区域整体向下移动,开辟数据区域,让上方映射区指向。
- 没有,以2倍扩充映射区域,将当前数据区域移动到中间。开辟数据区域,让上方映射区域指向开辟的数据区域。
【2. 尾部指针需要扩容----尾指针向下移动】 (和头部相反)
- 先看当前映射区的下面是否有空闲映射区,有,开辟一样大的数据区域,让下方映射区指向。
- 如果下方没有,看上方是否有映射区域,有,将数据区域整体向上移动,开辟数据区域,让下方映射区指向。
- 没有,以2倍扩充映射区域,将当前数据区域移动到中间。开辟数据区域,让下方映射区域指向开辟的数据区域。
(三)特点
根据deque的操作时间复杂度,我们可以总结出deque的优缺点:
操作 | 时间复杂度 |
---|---|
push_front | O(1) |
push_back | O(1) |
insert | O(n) |
pop_front | O(1) |
pop_back() | O(1) |
erase() | O(n) |
访问 | O(1) |
- 优点:直接在尾部或头部快速插入或删除,直接访问任何元素。
- 缺点:按位置插入和删除,时间效率低。
根据顺序容器的特性,可以分析容器适配器底层为什么默认为deque:
- 容器适配器是栈,队列,优先级队列,需要满足先进后出,先进先出等特性,vector是矢量容器,只支持在一端进行操作,所以不选它。
- list和deque都可以快速在头部,尾部插入与删除,但是deque支持随机访问迭代器,随机访问元素速度高,栈和队列对其需求大,所以选择deque。
(四)演示
# include<iostream>
# include<deque>
template <typename T>
void show( std::deque<T>& deq)
{
std::deque<T>::iterator it=deq.begin();
while(it!=deq.end())
{
std::cout<<*it<<" ";
it++;
}
std::cout<<std::endl;
}
int main()
{
std::deque<int> ideq;//无参构造
std::deque<double> ddeq(10);//一个int参数的构造
std::deque<char> cdeq(10,'c');//两个int参数的构造
int a[]={0,1,2,3,4,5,6,7,8,9,10,11,12};
int len=sizeof(a)/sizeof(a[0]);
std::deque<int> deq(a,a+len);//通过迭代器区间构造
//1.数组打印字符迭代器
for(int i=0;i< cdeq.size();i++)
{
std::cout<<cdeq[i]<<" ";
}
std::cout<<std::endl;
//2.迭代器打印整型容器
show<int>(deq);
//3.尾插10
deq.push_back(10);
std::cout<<"尾插10"<<std::endl;
show<int>(deq);
//头插200
deq.push_front(200);
std::cout<<"头插200"<<std::endl;
show<int>(deq);
//4.迭代器指向起始位置,将数组的元素插入2号下标
std::deque<int>::iterator it=deq.begin();//操作完成后就会失效
deq.insert(it+2,a,a+5);
show<int>(deq);
//5.在0号下标插入2个100
std::deque<int>::iterator it1=deq.begin();
deq.insert(it1,2,100);//it迭代器已经失效了,所以重新定义迭代器
show<int>(deq);
//6.删除前0~9之间的元素元素
deq.erase(deq.begin(),deq.begin()+10);//删除0~9
show<int>(deq);
//5.删除第2个元素
deq.erase(deq.begin()+2);
show<int>(deq);
//6.删除尾部元素
deq.pop_back();
show<int>(deq);
//删除头部元素
deq.pop_front();
show<int>(deq);
//7.扩容,扩大空间,并初始化为0
std::cout<<"原来大小:"<<deq.size()<<std::endl;
deq.resize(20);//扩容到20个大小
std::cout<<"现在大小:"<<deq.size()<<std::endl;
show<int>(deq);
//9.清除
deq.clear();
show<int>(deq);
}
加油哦!🥡。