35.c/c++程序员面试宝典-容器
STL是一个标准的c++库,容器是其中的一个重要组成部分。在实现容器时主要提供了顺序容器和关联容器。STL为容器的遍历提供了迭代器、STL也提供了100多种算法的实现。这些算法包括有顺序、遍历等。
面试题132 什么是容器****
分析:容器是容纳特定类型对象的集合,因此容器中的对象必须是同一类型,而且该类型必须是可拷贝构造和可赋值的,所以包括的类型有内置的基类数据类型和带有公用拷贝构造函数和赋值操作符的类。STL中提供的容器有vector、list。deque、set、multiset、map、multimap等。
在STL中,容器一般用模板类来实现。不过STL并没有采用面向对象的技术,所以在STL中并没有一个通用的容器类,各种具体的容器也没有统一的基类。
容器可以视为数组的扩展,即对象的数组(广义数组),其中的元素(对象)通过容器对“[]”的重载,可以和数组一样利用下标(索引)来访问。
注意:STL中的容器可以包含的类型有内置的基本数据类型和带有公用拷贝构造函数和赋值操作符的类。
在STL中提供了两种类型的容器:顺序容器和关联容器。这两种分类下面是各种具体的容器:
(1)顺序容器,指的是将一组具有相同类型T的对象,以严格的线性形式组织在一起。顺序由容器可以视为数组和链表的推广。包括有一下3种顺序容器。
vector<T>;
deque<T>;
list<T>
(2)关联容器,提供一个key(键)实现对元素的随机访问,其特点是key是有序的,即元素是按预定义的键顺序(例如升序)插入的。关联容器具有从基于键的集合中快速提取对象的能力,其中集合的大小在运行时是可变的。关联容器可以视为关联数组、映射或字典的推广,它们保存的都是键值对,给定了其中的一个别称为键的值,就可以快速访问与其对应的另一个键的值。STL中的关联容器有以下4种。
1)set<Key>(集合):支持唯一键值,并提供对象本身的快速检索,例如set<long>{学号}(set类的头文件<set>);
2)multiset<key>(多重集合):支持可重复键值,并提供对键本身的快速检索;例如set<string>;{姓名}(可能的同名的)(multiset类的头文件是<set>);
3)map<Key,T>:支持唯一Key类型的键值,并提供对另一个基于键的类型T的快速检索;例如map<long,string>:{(学号,姓名)}、{(电话号码,姓名)}等(map类的头文件是<map>);
4)multimap<Key,T>(多重映射):支持可重复Key类型的键值,并提供对另一个基于键的类型T的快速检索;例如map<string,string>:{(姓名,地址)}、{(姓名,年龄)}等(multimap类的头文件是<map>)。
除了这两种以外,还有一种容器是容器适配器。不过容器适配器不是独立的容器,只是某种容器的变种,它提供原容器的一个专用的受限接口。特别是,容器适配器和普通容器的不一样是在于不提供迭代器。在STL中有3种容器适配器,具体如下。
1)stack<T>(栈):只支持top()(读取栈顶元素)、push()(在栈顶处加入新元素)和pop()(取出栈顶元素)操作(先入后出)的一种序列容器。(stack类的头文件是<stack>)。
2)queue<T>(队列):与stack类似,queue也是对序列容器的限制实现。与栈相比,队列也支持back()(读取队尾处的元素)和push_back()(在队尾处插入新元素)操作,但是不支持pop_back()(取出队尾处的元素)操作。不过,队列允许front()(读取队首处的元素)和pop_fornt()(取出队首处的元素)操作(前出后入)。(queue类的头文件是<queue>)
3)priority_queue<T>(优先队列):也是一种队列queue,不过其中的每个元素都被给定了一个优先级,用来控制元素到达队首top()的顺序。默认情况下,优先队列简单地使用运算符<进行元素比较,top()返回最大的元素。
注意:优先队列,并不要求其全部元素都是有序的,而只要求其第一个元素是最大的。
【答案】容器是容纳特定类型对象的集合,在STL中,有顺序容器和关联容器两种。
面试题133 有哪几种顺序容器****
分析:顺序容器,指的是将一组具有相同类型T的对象,以严格的线性形式组织在一起。顺序由容器可以视为数组和链表的推广。包括3种顺序容器:vector<T>、deque<T>和list<T>。
vector是一种顺序容器,存放的元素是以连续的数组的方式存储的,也就是说可以通过v[i]的方式访问vector中的元素,此时vector的访问时间是非常小的。而在对特定的值进行访问时,只能通过遍历vector的方式进行比较,这样的操作会导致较大的时间开销。同时在对vector的中间插入或者删除元素时,由于需要保持元素的连续型,通常会导致vector把后面的元素复制一遍甚至与重新分配的数组。因此,在随机插入方面vector相对来说性能较低。
注意:vector多用于不需要对数据进行随机增、删的场合。
如果要在程序中使用vector,必须在包含下面的头文件:
#include<vector> //包含头文件
vector属于std命名域的,因此需要通过命名限定,代码如下:
using std::vector; //使用命名限定
vector<int> vInts;
或者连在一起,使用全名,代码如下:
std::vector<int> vInts;
还可以是<iostream>中的cout一样使用全局的命名域方式:
using namespace std;
有多种方法可以创建vector容器,下面介绍几种常见的。
创建一个Widget类型的空的vector对象:
vector<Widget> vWidgets; //空的vector对象
创建一个包含500个Widget类型数据的vector:
vector<Widget> vWidget(500); //包含500个对象的vector
创建一个包含500个Widget类型数据的vector,并且都初始化为0:
vector<Widget> vWidgets(500,Widget(0)); //包含500个对象的vector,并初始化为0
利用现有的vector创建一个拷贝:
vector<Widget> vWidgetsFromAnother(vWidgets); //利用现有的vector创建一个拷贝
vector是一种容器,那么可以向vector添加一个数据。vector添加数据的默认方法是push_back()。push_back()函数表示将数据添加到vector的尾部,并按需要来分配内存。例如,在vector<Widget>中添加10个数据,需要如下编写代码:
for(int i=0;i<10;i++)
v.push_back(Widget(i)); //循环10次添加10个数据
很多时候不必要知道vector里面有多少数据,因为vector中的数据是动态分配的,使用push_back()将导致vector的数据持续增加。如果想要知道vector是否存放两种数据,可以使用empty()函数。要获取vector的大小,可以使用size(),如果想获取一个vector v的大小,但不知道它是否为空,或者已经包含了数据。如果为空想设置为0,可以使用下面的代码:
int nSize=v.empty()? - 1:static_cast<int>(v.size()); //判断是否包含数据,并返回相应的值
访问vector的数据有如下两种方法。
vector::at();
vector::operator[].
operator[]主要是为了与c语言进行兼容。使用该操作可以像c语言数组一样操作。但at()是首先,因为at()进行了边界检查,如果访问超过了vector的范围。将抛出一个异常,而operator[]则不会。因此operator[]容易造成一些错误,一般不使用它,如下代码所示:
vector<int> v; //定义vector对象
v.reserve(10); //分配空间但没有初始化
for(int i=0;i<7;i++)
{v.push_back(i);}
try
{
int iVal1=v[7]; //不进行边界检查,将不会抛出异常
int iVal2=v.at(7); //检查vector的边界,并抛出异常
}
catch(const exception& e)
{cout<<e.what();}
vector能够非常容易地添加数据,也能很方便地取出数据。同样vector提供了erase(),pop_back(),clear()来删除数据。当删除数据的时候,需要知道要删除尾部的数据,或者是删除所有数据,还是个别的数据。
deque容器是一个双端队列,存放的数据不是以连续的形式存放的。其操作接口和vector类似。
list容器:一种链表的实现,存储的元素是通过使用双向链表实现的。双向链表的每个结点存储一个元素。其优势在于在list的任意位置进行插入和删除时,非常的快速。但因此也有一个问题是,对于list中的内容只能通过连续的方式对元素进行存取。例如需要对list中的第1000个元素进行访问,并修改其中的数据,那么只能从list的开头开始一个一个地移动。直到到达第1000个元素为止。因此list在查找和随机存取时需要耗费更大的开销。
【答案】顺序容器,指的是将一组具有相同类型T的对象,以严格的线性形式组织在一起的容器,包括vector、deque和list等3种顺序容器。
面试题134 什么是迭代器的范围****
分析:迭代器是STL提供的对一个容器中的对象的访问方法,并且定义了容器中对象的范围。迭代器就如同一个指针。事实上,c++的指针也是一种迭代器,但是,迭代器不仅仅是指针,因此不能认为迭代器是一定具有地址值。
迭代器有各种不同的创建方法。程序可能把迭代器作为一个变量创建。一个STL容器类可能为了使用一个特定类型的数据而创建一个迭代器。迭代器类似与指针一样,能够使用*操作符类获取数据。迭代器有两个是已定义好的。一个是begin,另一个是end。可以通过容器的bigin()操作和end()操作获取这两个位置。其中begin()指向容器中的第一个元素,end指向的是容器中的最后一个元素的下一个位置,也就是说end做指向的并不是容器的元素,通常begin和end之间的范围就是迭代器的范围。
可以使用迭代器对容器进行遍历,如以下代码所示:
vector<int> v; //声明vector变量
v.push_back(2); //往vector变量中插入数据
v.push_back(1);
vector<int>::iterator first=v.begin(); //获取vector<int>的第一个元素的迭代器
while(first!=v.end()) //使用迭代器遍历vector,一直到最后一个元素
{
int i=*first; //获取迭代器指向的元素的值
first++; //first迭代器指向下一个元素
}
注意:在使用迭代器之前,不应该存储操作end()的返回值,因为如果在保存了end()的返回之后,又对容器的元素进行插入和删除,所以这样使用的话,将会导致end()迭代器失效。
使用旧的end()值很可能所指向的元素已经不存在或者死循环。如下代码所示是错误的。
vector<int> v; //声明vector变量
v.push_back(2); //往vector变量中插入数据
vector<int>::iterator first=v.begin(); //获取vector<int>的第一个元素的迭代器
vector<int>::iterator last=v.end(); //存储end()的返回值是不正确的
while(first!=end) //使用迭代器遍历vector,一直到最后一个元素
{
int i=*first; /获取迭代器指向的元素的值
v.push_back(1); //这是不正确的
first++; /first迭代器指向下一个元素
}
应当是在每次进行了插入或者删除操作后,都应该重新计算end()。如下所示:
vector<int> v; //声明vector变量
v.push_back(2); //往vector变量中插入数据
v.push_back(1);
vector<int>::iterator first=v.begin(); //获取vector<int>的第一个元素的迭代器
while(first!=end)
{
int i=*first; /获取迭代器指向的元素的值
v.push_back(1); //插入数据
first++; /first迭代器指向下一个元素
}
【答案】迭代器有两个可以通过容器的begin操作和end操作获取。其中begin指向容器中的第一个元素,end指向的是容器中的最后一个元素的下一个位置,也就是说end所指向的并不是容器的元素。通常begin和end之间的范围就是迭代器的范围。
面试题135 什么是关联容器****
分析:关联容器支持通过键值(关键字)来高效地查找和读取元素。在STL中有两个基本的关联容器,分别是map和set。
map的元素是“键-值”对的二元组形式即键用作元素在map中的索引,而值则表示所存储和读取的数据。set仅包含一个键,并有效地支持关于某个键是否存在的查询。set和map类型的对象所包含的元素都具有不同的键。如果需要一个键对应多个实例,则需要使用multimap或multiset类型。这两种类型允许多个元素拥有相同的键。
set是一个容器,它其中所包含的元素的值是唯一的。集合中的元素按一定的顺序排列,并被作为集合中的实例。一个集合通过一个链表来组织,在插入操作和删除操作上比vector快,但查找或添加末尾的元素时会有些慢,以下示例代码演示了set的使用。
#include<string>
#include<set> //包含set的头文件
using namespace std;
int main()
{
set<string> strset; //声明一个set的变量
set<string>::iterator s1; //声明一个set<string>的迭代器
strset.insert("1"); //往set中添加数据
strset.insert("2");
strset.insert("3");
strset.insert("4");
strset.insert("5");
strset.insert("6");
for(s1=strset.begin();s1!=strset.end();s1++) //使用迭代器对set进行遍历
{cout<<*s1<<" ";}
cout<<endl; //输出set中的所有数据
return 0;
}
map:经过了排序的二元组的集合,map中的每个元素都是由两个值组成,其中的key(键值,一个map中的键值必须是唯一的)是在排序或搜索时使用,它的值可以在容器中重新获取;而另一个值是该key关联的数值。例如,除了可以ar[43]="overripe"这样找到一个数据,map还可以通过ar["test"]="sample"这样的方法找到一个数据。
set容器和multiset容器、map容器和multimap容器的区别如下:
set和map类型的对象所包含的元素都具有不同的键。如果需要一个键对应多个实例,则需要使用multimap或multiset类型。
例如以下代码所示:
set<int> strset; //声明set变量
strset.insert(1); //往set中插入两条相同的数据,这是不允许的
strset.insert(1);
multiset<int> mset; //声明multiset变量
mset.insert(1); //往multiset中插入两条相同的数据,这是可以的
mset.insert(1);
注意:关联容器主要用于需要使用键值对保存数据的场合,能够有效地随机存取数据。
【答案】关联容器是能够通过键值(关键字)来查找和读取元素的容器。在STL中有4个关联容器,分别是map和set、multimap和multiset容器。