文章摘自:c++从入门到精通(第四版,清华大学出版社)
数据结构和序列容器篇
目录
一.几种常见的数据结构
1.简述STL
标准模版库(Standard Template Library,STL),是根据本地c++标准规范定义的一套功能强大,适用范围广的函数模版、类模版的库,它主要包含容器、算法、函数对象等内容。
容器:实现了多个类型的数据结构和一些相关的操作。
算法:提供了排序、查找、替换等功能的函数。
函数对象:C++运算符重载的进一步应用,类模板实例化后很多工作都需要它的支持。
首先简单介绍一下一些数据结构的基本概念。
2.顺序线性结构
数据结构本身是一种集合,包含的各项称为元素。
数组是顺序线性结构的实现,通过指针来访问所有元素(对象),之后对它执行相应的操作。
3.基本操作
操作主要分为以下4种:
查询:通过输入或者已知条件在数据结构中找到相应的元素。
插入:在各个元素之间,逻辑插入一个元素。
删除:在各个元素中,逻辑删除一个元素。
修改:通过输入或者已知条件在数据结构中修改相应的元素。
注意:逻辑插入意味着元素所在的储存单元并不一定发生真的插入操作。例如数组本身所在的内存分布是连续的,无法在中间插入任何内存单元。实现插入的方法如下:将数组中所有元素和待插入的元素依照相应的次序复制给另一个容量+1的数组,之后就销毁原来的内存空间。新的数组可以被认为是完成插入后的数组,之后销毁原来的内存空间,新的数组可以被认为是插入完成之后的数组(一般需要动态分配),逻辑删除与它的原理相同。
4.栈结构
栈的结构如图所示,它很像一个从上方打开的盒子,所有的对象都遵循着后进先出的原则,在栈中随机位置插入和删除的实现通常需要辅助方法实现。相对较为缓慢。若所存储的对象超过自身容量,则会出现栈溢出。
5.队列结构
队列的结构和现实生活的排队一样,所有的对象都只能从队列的后方加入,从前方离开队列,即先进先出。和栈相似的是,队列不擅长处理随机位置的插入和删除操作,如图所示:
6.链表结构
链表与以上几种结构都是线性的结构。与他们不同的是,链表的储存方式不是顺序的,每个对象中含有下一个对象所在的位置信息(可认为是地址),链表的第一个元素的位置由头指针(也称为head)储存、链表尾部的元素储存的信息为空。由于链表的储存区不是连续的,因此无法通过指针偏移量的方案查找相应的对象,只能够通过以遍历的方法访问:链表由头指针所存储的地址信息访问到第一个元素,之后依次由所储存的位置信息(地址)访问后面的元素。链表的优点是在随机位置的插入和删除比较迅速。链表可分为单向链表、双向链表和循环链表。
7.图结构
图结构是一种复杂的数据结构,每个对象都可以与其他的对象相关联。图一般采用映射的方法表示内部的元素。映射通常指的是事物之间的联系,如图所示,每条边代表着两个物体所对应的关系,使用表达式<k,t>表示这种关系,k,t可以代表线的两端,也可以是线和物体,随着不同的定义,所代表的关系也不同。
以上简单介绍了几种常用的数据结构,在标准模版库中,容器类对它们进行了实现、拓展和封装。
二.序列容器
STL提供了很多容器,每种容器都要提供一组操作行为。序列(sequence)容器提供插入功能。其中的元素都是有序的。但并未排序。序列容器包括向量(vector)、双端队列(deque)和双向链表(list)。
1.对比容器适配器和容器
标准模版库的容器适配器与容器都是用来储存和组织对象的模板类。容器适配器和容器相比,限制的条件更多。容器适配器定义在相应的头文件中。
表1.容器适配器头文件内容
头文件 | 内容 |
queue | 定义了一些具有队列结构特征的类模板,其中包含了queue<T>,是一种单向队列priority_queue排列自身对象,最大值会被放在队列前端。 |
stack | 包含了stack<T>类模板,具有栈数据结构的特征。 |
容器适配器还被定义在容器的头文件中,这通常与容器的内部实现有关。
表2.容器头文件内容
头文件 | 内容 |
vector | vector是一个在必要时能够自动增加容量的数组。在随机位置上插入元素会花费很大的系统开销,其中定义了相应的适配器queue |
dequeue | dequeue是一个双向队列,与vector作用相似。但多出来了从队列前加入元素的特性,其中也定义了queue和stack |
list | list<T>是一种双向的链表,定义了适配器stack |
map | map<k,T>是一种关联容器,k表示关联的对象T所在map位置中位置的信息,值必须唯一。 |
set | set<T>表示的是一种对应关系,T就是这种关系的象征,它在set中唯一并且不能够被直接修改,只能够删除它,之后加入新的对象来达到目的。 |
在C++中使用标准模版库提供的容器,需要加入对应的头文件并使用名称空间std。容器适配器与容器在使用限制上最大的区别是是否支持迭代器。迭代器的行为类似于指针,通过它能够遍历容器中所有的元素,但容器适配器并不支持它。通常情况下,我们更倾向于使用容器而非容器适配器。
2.对比迭代器与容器
标准模版库中提供了四种迭代器,如下表:
表3.容器迭代器的分类
迭代器 | 功能 |
输入和输出迭代器 | 支持对象序列的读/写,仅能使用一次(不可重用)。支持自加运算符++来获得一个新的迭代,这样它方可进行下一次读/写。 |
前向迭代器 | 支持输入和输出迭代器的功能,还可以进行对象的访问和储存操作。前向迭代器可以重用,用来遍历容器。 |
双向迭代器 | 双向迭代器包含了前向迭代器的功能,支持自减运算符“--”,使它能反向遍历容器。 |
随机访问迭代器 | 包含了以上所有迭代器的功能。重载加减运算符,可以对容器内任何元素进行随机访问。它还支持索引运算符“[]"和比较运算符 |
这四种迭代器在功能上都是向上兼容的,越来越强大。容器自身的迭代器种类是依照容器的结构来决定的,vector包含的迭代器是随机访问迭代器,list包含的是双向迭代器,在queue中的迭代器则是前向迭代器。
3.向量类模版
向量(vector) 是一一种随机访问的数组类型,提供了对数组元素的快速、随机访问,以及在序利部快速、随机的插入和删除操作。它是大小可变的向量,在需要时可以改变其大小。
使用向量类模板需要创建vector对象,创建vector对象有以下几种方法。
std::vector<type> name;
该方法创建了一个 名为name的空vector对象,该对象可容纳类型为type的数据。例如, 为整型值创建一个空std:vector对象可以使用这样的语句:
std::vector<int>intvector
vector用法实现如下:
std::vector<type>name(size);//此方法用来初始化具有size元素个数的vector对象
std::vector<type>name(size,value);//此方法用来初始化具有size元素个数的vector对象,并将该对象的初始值设为value
std::vector<type>name(myvector);//此方法用来复制构造函数,用现有的向量myvector创建了一个vector对象
std::vector<type>name(first,last);//该方法创建了元素在指定范围中的对象,first表示起始范围,last表示结束范围
vector对象的主要成员继承于随机随机接入容器和反向插入序列,主要成员函数及说明见下表
表4.vector对象主要成员函数及说明
函数 | 说明 |
assign(first,last) | 用迭代器first和last所辖范围内的元素替换向量元素 |
assign(num,value) | 用value的num个副本替换向量元素 |
at(n) | 返回向量中第n个位置元素的值 |
back | 返回对向量末尾元素的引用 |
begin | 返回指向向量中第一个元素的迭代器 |
capacity | 返回当前向量最多可容纳的元素个数 |
clear | 删除向量中所有的元素 |
empty | 如果向量为空,则返回true值 |
end | 返回指向向量中最后一个元素的迭代器 |
erase(start,end) | 删除迭代器start和end所辖范围内的向量元素 |
erase(i) | 删除迭代器i所指向的向量元素 |
front | 返回对向量起始元素的引用 |
insert(i,x) | 把值x插入向量中由迭代器i所指明的位置 |
insert(i,start,end) | 把迭代器start和end所辖范围内的元素插入到迭代器i所指明的位置 |
insert(i,n,x) | 把x的n个副本插入到向量容器迭代器i所指明的位置 |
max_size | 返回向量的最大容量 |
pop_back | 删除向量的最后一个元素 |
push_back | 把x值放在向量末尾 |
rbegin | 返回一个反向迭代器,指向向量末尾元素之后 |
rend | 返回一个反向迭代器,指向向量起始元素 |
reverse | 颠倒元素顺序 |
resize(n,x) | 重新设置向量大小n,新元素的值初始化为x |
size | 返回向量大小(元素的个数) |
swap(vector) | 交换两个向量的内容 |
下面通过实例进一步学习vector模版类的使用方法。
#include<iostream>
#include<vector>
using namespace std;
int main(){
vector<int> v1,v2;//定义两个容器
v1.resize(10);//手动分配空间,设置容器元素最小值
v2.resize(10);
v1=vector<int>(8,7);
int array[8]={1,2,3,4,5,6,7,8};//定义数组
v2=vector<int>(array,array+8);//给v2赋值
cout<<"v1的容量"<<v1.capacity()<<endl;
cout<<"v1当前各项"<<endl;
for(int i=0;i<v1.size();i++){
cout<<" "<<v1[i];
}
cout<<endl;
cout<<"v2的容量"<<v2.capacity()<<endl;
cout<<"v2当前各项"<<endl;
for(int i=0;i<v2.size();i++){
cout<<" "<<v2[i];
}
cout<<endl;
v1.resize(0);
cout<<"v1通过resize函数变成0"<<endl;
if(!v1.empty())
cout<<"v1容量"<<v1.capacity()<<endl;
else
cout<<"v1是空的"<<endl;
cout<<"将v1容量扩展为8"<<endl;
v1.resize(8);
cout<<"v1当前各项"<<endl;
for(int i=0;i<v1.size();i++){
cout<<" "<<v1[i];
}
cout<<endl;
v1.swap(v2);
cout<<"v1和v2 swap了"<<endl;
cout<<"v1当前各项"<<endl;
for(int i=0;i<v1.size();i++){
cout<<" "<<v1[i];
}
cout<<endl;
cout<<"v1当前容量"<<v1.capacity()<<endl;
v1.push_back(3);
cout<<"从v1后加入了元素3"<<endl;
cout<<"v1容量"<<v1.capacity()<<endl;
for(int i=0;i<v1.size();i++){
cout<<" "<<v1[i];
}
cout<<endl;
v1.erase(v1.end()-2);
cout<<"删除了倒数第二个元素"<<endl;
cout<<"v1容量"<<v1.capacity()<<endl;
cout<<"v1当前各项"<<endl;
for(int i=0;i<v1.size();i++){
cout<<" "<<v1[i];
}
cout<<endl;
v1.pop_back();
cout<<"v1通过栈操作pop_back放走了最后的元素"<<endl;
cout<<"v1容量"<<v1.capacity()<<endl;
cout<<"v1当前各项"<<endl;
for(int i=0;i<v1.size();i++){
cout<<" "<<v1[i];
}
cout<<endl;
return 0;
}
4.双端队列类模板
双向队列(deque)是一种随机访问的数据类型,提供了在序列两端快速插入和删除操作的功能,它可以在需要的时候修改其自身的大小,主要完成标准C++数据结构中队列的功能。
使用双端队列类模板需要创建deque对象,创建deque对象有以下几种方法:
std::deque<type> name;
该方法创建了一个名为name的空deque对象,该对象可容纳数据类型为type的数据,例如,为整型值创建一个空的std::deque对象可以使用这样的语句。
std::deque<int>intdeque;
std::deque<type>name(size);//该方法创建一个大小为size的deque对象
std::deque<type>name(size,value)//该方法创建一个大小为size的deque对象,并将对象的每个值设为value
std::deque<type>name(mydeque);//该方法使用赋值构造函数,用现有的双端队列mydeque创建了一个deque对象
std::deque<type>name(first,last);//该方法创建了元素在指定范围内的双端队列,first表示起始范围,last表示结束范围
deque对象的主要函数及说明减下表:
deque对象主要函数及说明
函数 | 说明 |
assign(first,last) | 用迭代器first和last所辖范围内的元素替换双端队列元素 |
assign(num,val) | 用val的num个副本替换双端队列元素 |
at(n) | 返回双端队列中第n个位置的元素值 |
back | 返回对双端队列最后一个元素值的引用 |
begin | 返回指向双端队列第一个元素的迭代器 |
clear | 删除双端队列中的所有元素 |
empty | 如果双端队列为空,则返回true值 |
end | 返回指向双端队列最后一个元素的迭代器 |
erase(i) | 删除迭代器i所指向的双端队列元素 |
erase(start,end) | 删除迭代器start和end所指向的双端队列元素 |
front | 返回一个对双端队列第一个元素的引用 |
insert(i,x) | 把值x插入向量中由迭代器i所指明的位置 |
insert(i,n,x) | 把x的n个副本插入到双端队列中由迭代器i所指明的位置 |
max_size | 返回双端队列的最大容量 |
pop_back | 删除双端队列最后一个元素 |
pop_front | 删除双端队列第一个元素 |
push_back(x) | 把x放在双端队列末尾 |
rebegin | 返回一个反向迭代器,指向双向队列最后一个元素之后 |
rend | 返回一个反向迭代器,指向双向队列第一个元素 |
resize(n,x) | 重新设置双端队列大小n,新元素的值初始化为n |
size | 返回双端队列的大小 |
swap(vector) | 交换两个双端队列的内容 |
双端队列类模板的应用:
#include<iostream>
#include<deque>
using namespace std;
int main(){
deque<int> intdeque;
intdeque.push_back(2);
intdeque.push_back(3);
intdeque.push_back(4);
intdeque.push_back(7);
intdeque.push_back(9);
cout<<"Deque:old"<<endl;
for(int i=0;i<intdeque.size();i++){
cout<<"intdeque["<<i<<"]:";
cout<<intdeque[i]<<endl;
}
cout<<endl;
intdeque.pop_front();
intdeque.pop_front();
intdeque[1]=33;
cout<<"Deque:new"<<endl;
for(int i=0;i<intdeque.size();i++){
cout<<"intdeque["<<i<<"]:";
cout<<intdeque[i]<<endl;
}
cout<<endl;
return 0;
}
程序定义了一个空的类型为int的deque对象,然后用函数push_back把值插入deque对象中,并把deque对象显示出来,最后删除deque对象中的第一个元素,并把删除后的deque对象中第二个元素重新赋值。
5.链表类模版
链表(list)即双端链表容器,它不支持随机访问,访问链表元素要指针从链表的某个端点开始,插入和删除操作所花费的时间是固定的,和该元素在链表中的位置无关。list在任何位置的插入和删除动作都很快,不像vector只在末尾操作。
使用链表类模版需要创建list,创建list对象有以下几种方法,
std::list<type>name(size);
该方法创建了一个名为name的空list对象,该对象可容纳数据类型为type的数据,例如,为整型值创建一个空std::list对象可以使用这样的语句:
std::list<int>intlist;
std::list<type>name(size);//该方法创建一个大小为size的list对象
std::list<type>name(size,value)//该方法创建一个大小为size的list对象,并将对象的每个值设为value
std::list<type>name(list);//该方法使用赋值构造函数,用现有的链表mylist创建了一个deque对象
std::list<type>name(first,last);//该方法创建了元素在指定范围内的链表,first表示起始范围,last表示结束范围
list对象的主要成员函数及说明见下表:
函数 | 说明 |
assign(first,last) | 用迭代器first和last所辖范围内的元素替换链表元素 |
assign(num,val) | 用val的num个副本替换链表元素 |
back | 返回对链表最后一个元素值的引用 |
begin | 返回指向链表第一个元素的迭代器 |
clear | 删除链表中的所有元素 |
empty | 如果链表为空,则返回true值 |
end | 返回指向链表最后一个元素的迭代器 |
erase(i) | 删除迭代器i所指向的链表元素 |
erase(start,end) | 删除迭代器start和end所指向的链表元素 |
front | 返回一个对链表第一个元素的引用 |
insert(i,x) | 把值x插入向量中由迭代器i所指明的位置 |
insert(i,n,x) | 把x的n个副本插入到链表中由迭代器i所指明的位置 |
max_size | 返回链表的最大容量 |
pop_back | 删除链表最后一个元素 |
pop_front | 删除链表第一个元素 |
push_back(x) | 把x放在链表末尾 |
rebegin | 返回一个反向迭代器,指向链表最后一个元素之后 |
rend | 返回一个反向迭代器,指向链表第一个元素 |
resize(n,x) | 重新设置链表大小n,新元素的值初始化为n |
size | 返回链表的大小 |
swap(vector)或 swap(listref) | 交换两个链表的内容 |
可以发现,list<T>所支持的操作与vector<T>很相近,但这些操作的实现原理不尽相同,执行效率也不一样,list双向链表的有点是插入元素的效率很高,缺点是不支持随机访问。也就是说,链表无法像数组一样通过索引来访问,形如:
list<int>list1(first,last); //初始化
list[i]=3;//错误!无法使用数组符号
对list各个元素的访问,通常使用的是迭代器。
迭代器的方法类似于指针。
注意:end()成员函数返回指向末尾位置的迭代器。这个“末尾位置”指的是最后一个元素再往后一位,也就是说end()所指的位置不包含有效元素,它相当于一个虚设的节点。这样设计是为了满足C++标准库表示区间时左闭右开的惯例。
实例:list和vector中的迭代器。
#include<iostream>
#include<list>
#include<vector>
using namespace std;
int main(){
cout<<"使用未排序储存0-9的数组初始化list1"<<endl;
int array[10]={1,3,5,7,8,9,2,4,6,0};
list<int>list1(array,array+10);
cout<<"list 调用sort方法排序"<<endl;
list1.sort();
list<int>::iterator iter=list1.begin();
cout<<"通过迭代器访问list双向链表中从头开始向后的第四个元素"<<endl;
for(int i=0;i<3;i++){
iter++;
}
cout<<*iter<<endl;
list1.insert(list1.end(),13);
cout<<"在末尾插入数字13"<<endl;
for(list<int>::iterator it=list1.begin();it!=list1.end();it++){
cout<<" "<<*it;
}
}
通过程序可以观察到,迭代器iterator类和指针用法相似,支持自增运算符++,并且可以通过“*”访问相应对象内容,但list迭代器不支持“+”号运算符,而指针与vector迭代器都支持。