【C++】二十三、标准模板库STL之概述、顺序容器

☛ 标准模板库STL框架

点开看更清楚🧐

在这里插入图片描述

☛ 顺序容器都有的函数

(一)构造函数

系统为顺序容器提供了4种构造方式,即定义方式,以vector为例:

  1. 默认构造: 直接开辟空间;如vector<int> vec
  2. 带有一个整型n的构造开辟n个大小的空间,用0初始化;如vector<int> vec(10),开辟10个空间,初始化为0
  3. 带有2个参数n,m的构造函数 :开辟n个空间,用m初始化。如vector<int>vec(10,20),开辟10个空间,用20初始化。
  4. 传入迭代器(指针)区间的构造函数传入起始位置,末尾位置,会把起始~末尾的迭代器区间元素传入容器。注意也可以传入数组,字符串的指针。如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),扩容流程:

  1. 开辟一定大小,
  2. 以倍数的形式开辟更大的空间,通常以2倍开辟
  3. 旧的数据拷贝到新空间中
  4. 释放旧空间
  5. 指针指向新空间,调整总大小。

(三)特点

根据vector的操作时间复杂度,我们可以总结出vector的优缺点:

操作时间复杂度
push_backO(1)
insertO(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_frontO(1)
push_backO(1)
insertO(1)
pop_frontO(1)
pop_back()O(1)
erase()O(1)
访问O(n)
  • 优点:任意位置快速的插入和删除元素。
  • 缺点:访问效率低

(三)list和vector的比较

  1. vector :底层是数组,数组内存空间连续,迭代器可以进行+i,-i的操作,是不会出错的,所以vector支持随机访问迭代器。
  2. list:底层是双向循环链表,内存不一定连续,故迭代器进行+i,-i操作时不一定能找到下一个结点,可能会发生内存越界的问题。所以list不支持随机访问迭代器,支持双向迭代器,只能提供++,–方式访问内存。
比较角度vectorlsit
底层结构动态顺序表,一段连续的空间带头结点的双向链表
随机访问支持随机访问,访问时间复杂度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_frontO(1)
push_backO(1)
insertO(n)
pop_frontO(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);
}

在这里插入图片描述

加油哦!🥡。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值