C++顺序容器的操作及总结

基于C++ primer第九章的总结

1. 顺序容器概述

顺序容器类型:

  • vector:可变大小数组,支持快速随机访问。速度慢
  • deque:双端队列,指出快速随机访问。速度慢
  • list:双向链表,只支持双向顺序访问。开销大
  • forword_list:单向链表,只支持单向顺序访问。开销大
  • array:固定大小数组,支持快速随机访问。
  • string:与vector类似,只能保存字符

使用哪种容器?

  • 一般情况下应该使用vector
  • 需要在中间位置插入删除元素选择 list,forword_List
  • 空间的开销很重要则不要选择 list,forword_List
  • 随机访问元素,选择vector或deque
  • 在头尾插入删除,不会在中间位置操作,选择deque
  • 特殊情况下,那种操作执行多,则选用那种容器

2. 所有顺序容器都支持的操作

迭代器范围

迭代器有公共接口,操作适用于大多数容器,forword_list不能使用迭代器的–运算符
迭代器支持的算术运算,只适用于string vector deque array等迭代器。

begin和end迭代器:begin指向首元素;end指向尾元素之后的位置

为什么? 为了防止容器中只有一个元素,being和end不相等时,容器至少含有一个元素

构成范围的迭代器的要求:

  • begin和end必须指向容器中的某个元素,或是最后的位置
  • begin通过递增可以到达end,begin一定在end的前面

让一个迭代器查找指定的元素

#include <iostream>
#include <list>
#include <deque>
#include <vector>
using namespace std;

vector<int>::iterator VectorFindNum(vector<int>& vec, int a);
int main()
{
	vector<int> a{ 1,2,3,4,5,6,7,8,9 };
	int num = 15;
	auto itx = VectorFindNum(a, num);
	if (itx == a.end())
	{
		cerr << "未找到!\n";
		return -1;
	}
	cout << *itx << endl;
	return 0;
}
vector<int>::iterator  VectorFindNum(vector<int>& vec, int a)
{
	vector<int>::iterator it;
	for (it = vec.begin(); it != vec.end(); ++it)
	{
		if (*it == a)
		{
			return it;
		}
	}
	return vec.end();
}

2.1 begin和end成员

形成一个包含迭代器的所有元素的迭代器范围

使用auto时:当我们希望对非const对象生成迭代器时,使用begin和end,只能得到iterator版本;使用cbegin和cend可以得到const_iterator的版本;对const的对象使用,一定会生成一个const_iterator版本

注意:对于同一行的auto,一个容器有const,另一个没有,则it2会报错,因为 it2会生成const_iterator版本,与前面的非const版本不同

	list<int> a{ 1,2,3,4,5,6 };
	const list<int> b{ 1,2,3 };
	auto it = a.begin(), it2 = b.begin(); //错误

2.2 容器的定义和初始化


一个容器初始化为另一个容器的拷贝

使用构造函数的方式:两个容器的类型和元素类型必须匹配!!

使用迭代器范围的方式:容器类型和元素类型可以不必匹配,因为获取的是元素。

	list<int> a{ 1,2,3 };
	vector<double> b{ 1.1,2.2,3.3 };
	
	list<int> a1(a);
	list<double> a2(a);	//元素类型不匹配
	vector<int> a3(a);	//容器类型不匹配

	vector<int> b1(b.begin(), b.end());	
	list<string> b2(b.begin(), b.end());

与顺序容器大小相关的构造函数

提供一个初始大小与初始化值,如果未提供值,则会自动创建一个值初始化器(即自动规定数值)

标准库array

array必须要在定义时规定大小,因为其大小是固定的。

可以进行列表初始化,但要小于等于此大小,不能超过大小限制

array也支持拷贝与赋值

	array<int, 10> a{ 1,2,3,4,5 };
	array<int, 3> b{ 1,2,3,4 };	//越界

	//支持拷贝
	array<int, 10> c(a);

简单总结

六种初始化和赋值容器的方式,除了array,皆适用

vector<int> vec;    // 0
vector<int> vec(10);    // 0
vector<int> vec(10,1);  // 1
vector<int> vec{1,2,3,4,5}; // 1,2,3,4,5
vector<int> vec(other_vec); // same as other_vec
vector<int> vec(other_vec.begin(), other_vec.end()); // same as other_vec

2.3 赋值和swap

赋值相关运算适用于所有容器

在赋值中,左侧对象会变成右侧的元素对象拷贝

即:自动改变长度大小

	list<int> a{ 1,2,3,4,5 };
	list<int> b{ 1,2,3 };
	a = b;	//a自动变为与b一样的大小
	a = { 1,2,3,4,5,6,7,8,9 };//自动改变大小

注意array除外:他不支持改变大小

	array<int, 5> ar{ 1,2,3,4,5 };
	array<int, 2> br{ 1,2 };
	ar = br;	//不支持
	ar={0};		//不支持

使用assign

他将右边运算对象中的元素拷贝到左边运算对象,也就是说实现了迭代器的使用(array除外)

利用迭代器拷贝元素值

	list<string> name;
	vector<const char*> str{ "我爱你","于良浩" };

	//实现了将vector的const char*赋值给了list的string
	name.assign(str.begin(), str.end());
	name.assign(name.begin(), name.end());

第二个版本:接受大小与初始值

清除原来的元素,然后再插入10个"www",相当于先clear再insert

name.assign(10, "www");

swap交换容器

速度很快:元素本身并未改变,只是交换了两个容器的内部数据结构

	list<int> a(10);
	list<int> b(20);

	//交换容器,包括大小即数值
	a.swap(b);
	swap(a, b);

如果两个对象具有指针或者引用,迭代器所指向的对象不变!!

即使被swap交换,则仍然指向一个开始指向的对象(专一)

2.4 关系运算

比较容器间及其每个元素之间的关系,每个容器都支持==和!=
相比较的两个容器必须相同类型


3. 顺序容器所特有的操作


3.1 向容器添加元素

  • 注意:array不支持!插入一个元素后,原有的指针 引用和迭代器都会失效**

push_back

将一个元素追加到容器的尾部,array和forword_list不支持此方法

可用于list vector deque的尾部插入,也可以在string对象后面插入一个字符

	list<int> a;
	for (int i = 0; i < 5; i++)
	{
		a.push_back(i);
	}
	for (auto x : a)
	{
		cout << x << " ";
	}
	string str{"woaini"};
	str.push_back('d');		//在str的后面插入字符d

插入对象的值实际上是一份拷贝,与原始的提供值的对象无任何关联

push_front

可用于list; forword_list; deque支持。vector 和string不支持

	forward_list<int> a;
	for (int i = 0; i < 5; i++)
	{
		a.push_front(i);
	}
	for (auto x : a)
	{
		cout << x << " ";
	}

insert插入

普通版本

list dewue vector string 都提供了这种操作,用于在其他位置插入。forword_list提供了另一种版本的insert

  • 第一个参数:一个迭代器
  • 第二个参数:一个插入的值
  • 将一个值插入到迭代器的前面位置的元素

可以在不能使用头插的vector使用insert来达到头插。

注意:vector deque string使用insert是合法的,但是速度会很慢!!!!

vector<int> a;
for (int i = 0; i < 5; i++)
{
	a.insert(a.begin(), i);
}

进阶版本

insert接受更多参数:指定数量的数值,迭代器范围,初始化列表

//插入范围内的元素
list<int> a{1,2,3};
vector<int> b{ 4,5,6 };
a.insert(a.begin(), 5, 1);					//在a的前面插入5个1	
a.insert(a.end(), b.begin()+2, b.end());	 //在a的尾部插入b的迭代器范围的元素
a.insert(a.begin(), { 9,9,9 });				//在a的尾部插入一个参数列表

注意:VS2022 可以让迭代器指向自身的范围:(书中有误)

a.insert(a.begin(), a.begin(),a.end());		//不会出错,相当于复制二倍

使用返回值

用一个迭代器来隐式指明插入位置,并在每一次循环返回当前位置的迭代器,iter迭代器始终指向begin元素,相当于push_front头插

list<int> a{ 4,5,6 };
auto iter = a.begin();
for (int i = 3; i > 0; i--)
{
	iter = a.insert(iter, i);
}

emplace插入

也是插入的一种形式,不过与push不同的是,它处理的是类的构造函数的插入:

  • push将插入的元素拷贝到要插入的容器中

  • empl/ace将插入的元素作为某个对象的构造函数插入到容器中

    struct io
    {
    	int a;
    	io() = default;
    	io(int i) :a(i) {}
    };	//一个具有构造函数的类
    vector<io> a;
    vector<io> b;
    a.emplace_back();	//调用无参构造函数,将创建的对象插入容器中
    a.emplace_back(9);	//调用构造函数
    for (int i = 0; i < 5; i++)
    {
    	b.emplace_back(i);
    }
    a.emplace(a.begin(),7);	//两个参数的版本,调用构造函数生成对象插入到指定位置之前
    

3.2 访问元素

两种方式:

  • 使用迭代器begin和end,begin指向第一个,end递减依次后指向最后一个
  • 使用front和back成员函数,front指向第一个,back指向最后一个(返回的是头或尾的引用
  • 不能对空容器调用front和end

下标操作和安全的随机访问: 适用于string deque vector array等容器

vector<int> a{ 1,2,3 };
cout << a[1] << endl;		//重载了[]运算符
cout << a.at(2) << endl;	//at访问
cout << a.at(4);			//out of range

3.3 删除元素


forword_list不支持pop_back;vector和string不支持pop_front

pop_front和pop_back

删除第一个或者最后一个元素

vector<int> a{ 1,2,3 };
while (!a.empty())
{
	a.pop_back();
}		//删除容器所有元素

erase删除

可以指定迭代器指定的元素,也可以删除指定范围的元素

vector<int> a{ 1,2,3 };
auto it = a.begin() + 1;	//指向第二个元素
a.erase(it);		

删除多个元素

erase:删除指定范围,返回最后一个被删元素之后位置的迭代器

clear:删除所有元素

简单总结

使用单迭代erase 删除list容器的奇数元素和vector的偶数元素,注意erase的返回值,返回被删的元素之后的元素迭代器

int ia[] = { 0,1,1,2,3,5,8,13,21,55,89 };
	list<int> a;
	vector<int> b;
	for (auto x : ia)
	{
		a.push_back(x);
		b.push_back(x);
	}
	auto ita = a.begin();
	auto itb = b.begin();
	while (ita != a.end())
	{
		if (*ita % 2)
		{
			ita=a.erase(ita);
		}
		else
		{
			++ita;
		}
	}
	while (itb != b.end())
	{
		if (*itb % 2==0)
		{
			itb=b.erase(itb);
		}
		else
		{
			++itb;
		}
	}

4. forward_list的特殊操作

他是一个单向链表,在删除或添加元素的时候,需要改变其他的元素的连接,因此没有直接插入和直接删除的函数,它提供了insert_after emplace_after erase_after ;需要一个迭代器,然后插入在他的后面,或者删除它的后面

  • before_begin:返回一个首前迭代器,即首元素的前面。
  • insert_after:返回新插入的元素的迭代器
  • erase_after:返回删除后的下一个元素的迭代器

删除foward_list的奇数元素

forward_list<int> a{ 1,2,3,4,5,6,7,8,9 };
auto prev = a.before_begin();		//一个前驱迭代器
auto curr = a.begin();				//一个用于遍历的迭代器
while (curr != a.end())
{
	if (*curr % 2)
	{	
        //删除节点,则必须传递给他前驱的节点,返回删除后的下一个元素的迭代器
		curr = a.erase_after(prev);
	}
	else
	{
		prev = curr;
        ++curr;
	}
}

5. resize改变容器大小


array不支持resize

如果重置后的大小增大了,可以指定一个值参数, 放在后面新增的空间中,也可以是默认的

如果重置后大小减小,则直接删除多余的部分

list<int> a(10, 5);
a.resize(20, 1);
a.resize(5);

6. 容器操作会使迭代器无效


  • vector和string,deque容器:添加删除元素在任意位置都会导致迭代器失效,但是指针和引用仍然有效
  • list和forward_list容器:添加删除元素在任意位置,迭代器,指针和引用均有效
  • 每次在添加元素后, 要重新定位迭代器位置:vector string deque

示例:改变容器的循环程序

  • 插入删除元素后更新使用新的迭代器的例子

  • insert 插入返回原始迭代器

  • erase删除返回下一个元素迭代器

    vector<int> a{ 0,1,2,3,4,5,6,7,8,9 };
    auto iter = a.begin();
    while (iter != a.end())
    {
    	if (*iter % 2)
    	{
    		iter = a.insert(iter, *iter);
    		iter += 2;		//移动迭代器到新插入的元素和原来插入前指向的元素之后
    	}
    	else
    	{
    		iter = a.erase(iter);
    	}
    }
    

    不要保存end返回的迭代器


7. Vector对象是如何增长的


管理容量的成员函数

  • capacity:在不扩张内存的情况下,可以容纳多少个元素
  • reserve:通知容器他应该准备多少个元素的内存空间
  • shrink_to_fit:将capacity减少为与size相同大小
  • 注意:resize改变容器的大小,但并不改变内存空间

capacity空间分配函数

  • size:已经保存的元素的数目
  • capacity:在不分配内存空间的前提下,它最多可以保存多少个元素

capacity预保留一些空间,它要至少与size一样大

	vector<int> a;
	cout << "size: " << a.size() << "\t" << "capacity: " << a.capacity() << endl;
	for (vector<int>::size_type i = 0; i != 24; ++i)
	{
		a.push_back(i);
	}
	cout << "size: " << a.size() << "\t" << "capacity: " << a.capacity() << endl;

在这里插入图片描述

reserve预分配空间:capacity至少等于reserve,也可能更大

a.reserve(50);

在这里插入图片描述
在超出预分配空间后,再次添加数据,则会重新分配空间

注意:重新分配的额外空间是reserve预分配空间的一半(VS2022)

shrink_to_fit:释放多余的空间,等于size的大小

resize:减小容器的容量,并不影响分配的空间;增大容量,内存空间会自动增大到resize(至少)


8. 额外的string操作(再补充)


string修改操作

string的构造函数:不再赘述

substr:返回一个string的一部分;或是全部的拷贝

insert,assign(所有内容),erase在string同样使用,提供了改变string的方法

append:在末尾追加字符串。

replace:相当于替换(erase和insert结合),在某个位置删除原字符并且插入新字符

string搜索操作

每个搜索操作都返回一个string::size_type的值,表示匹配发生位置的下标;

搜索失败返回string::npos的static成员

find:查找指定字符

find_first_of

find_first_not_of…简单看一下就好了


9. 容器适配器


三个容器适配器:stack,queue,priority_queue

定义适配器

从deque容器的类型元素拷贝到stack类型

deque<int> deq{1,2,3};
stack<int> stk(deq);

创建一个适配器时,将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型

从vector上实现的空栈

stack<string, vector<string>> str_stk;

构造方式

适配器要有添加和删除操作,并且要有访问尾元素的功能,因此不能构造在array和forward_list类型之上

queue和stack可以构造于list和deque(需要push_front等操作),不能基于vector构造

priority_queue可以构造于vector和deque(随机访问功能),不能基于list构造

栈适配器

入栈 出栈 获取栈顶元素 判断为空

//栈适配器
	stack<int> intStack;
	for (int i = 0; i < 10; i++)
	{
		intStack.push(i);
	}
	while (!intStack.empty())
	{
		int val = intStack.top();
		intStack.pop();
	}

队列适配器

//队列适配器
	queue<int> Que;
	for (int i = 0; i < 10; i++)
	{
		Que.push(i);
	}
	while (!Que.empty())
	{
		int val = Que.front();
		Que.pop();
	}

关于string得到操作和适配器的 最后一道习题,之后会单独写一篇博客

本章练习源码,戳这里下载!!!!!

posted @ 2022-08-08 17:05  hugeYlh  阅读(35)  评论(0编辑  收藏  举报  来源