要求:熟悉使用STL/boost泛型数据结构及算法
C++标准库的一个强大之处就是它包含了各种各样的容器和算法,并且都是泛型(Generic)的,可以实现泛型编程(Generic Programming)。所谓泛型编程,就是在编程时不需要考虑具体数据类型,不需要寻找并使用类型与当前变量匹配的算法,而算法使用的数据结构,也就是容器,也不需要根据数据类型重复不同版本。
C++标准库中,容器和算法所在的标准库子集又叫标准模板库(Standard Template Library),简称STL。这个库包括3种类型的通用项:容器、迭代器和算法。算法是可以应用于不同数据结构的常用函数,其应用是通过迭代器来协调的,迭代器决定了算法能够应用于哪些类型的对象。有了STL,程序员就不必编写自己的类和函数,而能够利用预先打包好的通用工具,来解决现有的问题。
容器是一种数据结构,存储具有相同类型的对象。不同类型的容器在其内部以不同的方式组织对象。STL中的容器分为顺序容器和关联容器两种。顺序容器通过元素的位置顺序存储访问,而这个顺序一般是由元素进入容器的顺序决定的。关联容器则通过键来查找键对应的元素。STL包括的容器有:deque、list、map、multimap、set、multiset、stack、queue、priority_queue和vector。
对于STL中的容器来说,有一种通用的遍历方法—使用迭代器(Iterator)。迭代器是一个对象,用于引用存储在容器中的元素。因此,它是一个通用指针。迭代器是声明在每个容器类定义中的,因此在定义的时候我们需要在类型名前面加上具体的容器名和作用域操作符。在循环中使用迭代器取容器元素的时候,我们可以把它看作指针,并使用解引用操作即可。
容器元素的类型需要满足两个基本条件:一是支持赋值操作符,二是支持复制操作。
clear()函数
clear()函数将会删除容器中所有的元素。
erase()函数
erase()函数用于删除一个或者部分元素。
insert()函数
insert()函数用于往迭代器中添加元素。
例子:
(1)插入一个3到第二个元素前面
vec.insert(vec.begin() + 1, 3)
(2)将刚才插入的3移除
vec.erase(vec.begin() + 1)
(3)插入三个3到第五个元素前面
vec.insert(vec.begin() + 4, 3, 3)
(4)将刚才插入的三个3移除
vec.erase(vec.begin() + 4, vec.end() - 1)
Erase-Remove惯用法
使用erase()删除是基于元素的位置信息决定的,但是在实际应用场景中,根据元素的值来删除会显得更加直观。在STL里这样的操作会显得比较繁琐,需要同时使用erase()和remove()来完成。
remove()有三个参数,分别界定了搜索的范围和查找的值。
swap()函数
swap()函数作用是交换两个容器的元素
例子:vecOdd.swap(vecEven)
vector是标准库中最常见的一种容器,使用起来非常方便,可以用来替代C++原本的数组。
vector的创建和初始化要考虑以下三个属性:数据的类型、数据的个数以及数据的值。
例子:vector的初始化
vector vec1; 空的整型vector,没用给它添加任何元素;
vector vec2(3); 初始化了一个有3个元素的vector,由于并没有指定初始值,将会使用编译器默认的初始值;
vector vec3(3, ‘a’); 初始化了含有3个a的字符vector,括号中第二个值代表了所有元素的指定初始值;
vector vec4(vec3); 通过拷贝的方式使用vec3中的元素初始化vec4,它们的元素将会一摸一样;
vector的实现与数组相似。数组的元素都是排列在一起的,所以随机访问某一个元素只需要首个元素的地址加上偏移量就能定位到。而在删除数组中元素的时候,我们需要将被删除元素后面的所有元素往前移动一格,不然数组就会有空隙。因此,我们知道vector适用于元素随机访问多但添加、删除中间元素少的程序。
我们之前都只是把vector当成数组来看待,但其实vector是一种动态的数据结构。我们知道数组的容量在声明的时候就是确定的,而动态数组的容量虽然在运行时才指定,但分配完后也就不能修改了,除非释放内存后重新分配。vector在底层实现动态数组其实也类似于这种方式,在初始容量占满之后就会重新分配更大的空间,然后将原有的元素复制过去,释放原来的空间,这样我们就可以一直使用push_back()添加元素却永远不会满了。但其实在底层,vector使用的空间就已经开始暗中变化了,而且不是一点点地增加。
我们可以发现,在每次添加元素的时候,vector的容量不是线性增加的。vector的元素越多,容量一次性增长就越快。动态增长大小的确定与内存cache的相关知识有关。除了等待容器满了以后自己触发容量扩张,在知道了vector可能使用的最大容量的时候,我们也可以使用reserve()函数手动指定vector的容量。
vector以及其他的顺序容器都支持assign操作。assign操作可以将容器中的现有内容删除,并用其他容器中一定范围内的元素填充,或是填充一定数目的相同元素。每次赋值前都会清空当前容器。assign的语义与赋值操作符的区别就在于赋值操作符只能将整个容器的元素都赋值,而assign可以选取其中的一部分。
将容器内的现有内容删除,并用其他容器中一定范围内的元素填充示例如下:vec.assign(vecSrc.begin(), vecSrc.end());
填充一定数目的相同元素示例如下:vec.assign(5, 6)将五个6assign给vec;
另一个顺序容器支持的操作是resize。resize会改变容器的大小,并截掉多余的部分或填补多出的部分。
顺序容器list与vector的不同之处在于list可以快速地添加和删除元素。list的底层是由一种重要的数据结构链表(Linked List)实现的。
数组和链表的区别:
数组有以下特点:
(1)数组是一块连续的区域。
(2)数组需要预留空间,可能会有空间没有数据或者数据不够放的情况。
(3)插入和删除效率低,需要移动后继的元素。
(4)随机访问效率高,因为每个元素地址都是已知的。
链表有以下特点:
(1)链表的每个节点都不需要连续,可以放在任何地方。
(2)不用预留空间,数据可随意增删。
(3)每个节点都存着下个节点的地址。
(4)访问需要顺着一个个节点找,不支持随机访问。
顺序容器deque可以说是list和vector的混合体,它的底层实现与vector类似,支持快速随机访问,而它也像list一样可以分别从两头快速地添加和删除元素。
与vector不一样的是,string不允许使用栈的方式处理元素,也就是不允许使用push_back()和back()等,而只能随机访问其中的字符。
string字符串的构造方法,举例如下:
(1)用已有字符串和起始字符位置构造
string s2(s1, 6);
(2)用已有字符串、起始字符位置和子字符串长度构造
string s3(s1, 6, 3);
string也有很多自己特有的函数。先来看一个非常实用的substr()函数,它可以截取字符串的子字符串。
关联容器pair,pair类型在中定义,代表着两个值组成的值对。
pair的初始化。pair在初始化时需要指定两个元素,这两个元素可以是任一类型的。
示例如下:
(1)pair
(2)pair
(3)pair
关联容器map,它的组成元素其实就是pair类型。map类型的特点是可以通过键来很快地找到值,在其中的每一个pair里,first就是键,而second就是值。键在map中是不能重复的。
map的结构是键值对,而set中只有键,set是键的集合。set的大多数初始化规则与操作都和map相同,只是不支持下标操作,因为set中并没有值可以让我们用下标操作符来获得,所以添加元素的时候我们只能使用insert()。map适用于取得键对应的值,而set则适用于判断键是否存在。
STL定义了很多泛型的算法可供用户使用,这些算法都是基于迭代器的,不使用任何与特定容器类型有关的变量或操作,所以它们得以独立于容器存在,这也就是所谓的泛型的特性。STL提供了大约70个通用函数,称为算法,这些算法能够应用于STL容器和数组。
使用accumulate()实现,这个算法可以在set和vector两种容器上使用,所需要的只是表示范围的两个迭代器而已。有了迭代器以后,就可以用遍历的方法访问范围内的所有元素并进行相应的操作,而不需要关注容器是什么。而accumulate()的第三个参数则是累加的一个基础值,这也是为了让编译器能够判断调用的accumulate()应该使用何种元素类型的容器。
这里的排序算法不单单指一般意义上的把容器元素排序的算法,而是指所有调用完后会改变容器内元素顺序的算法。
虽然unique()函数不是排序的函数,但是去重之后相邻元素移动了,所以也属于排序算法的类别。需要注意的是,unique()函数不会改变容器的大小,就和remove()一样,因此还是需要erase()来清理后面无效的元素。
在排序算法中,sort()应该算是最常用的一种算法。sort()的使用与unique()相似,一个乱序的vector在排序之后,元素会从小到大排列,这也是因为sort()算法默认使用小于操作符”<“比较迭代器指向的元素。如果我们想实现按其他规则排序,也可以指定自定义的比较函数。
sort()的第三个参数也不是只能传函数,也可以传一种叫作函数对象(Function Object)的类对象。函数对象实现了调用操作符,可以像函数一样使用,但也能有其他的成员变量来充当额外返回值或参数,也可以有辅助用的其他成员函数。