学习过C++的朋友们应该对STL和泛型编程这两个名词不会陌生。两者之间的关系不言而喻,泛型编程的思想促使了STL的诞生,而STL则很好地体现了泛型编程这种思想。这次想简单说一下STL在ACM中的一些应用。我们知道,在ACM竞赛中,经常需要用到数组、字符串、队列、堆栈、链表等数据结构和排序、搜索等算法,以提高程序的时间、空间运行效率。然而如果这些数据结构总是需要手工来编写,那无疑会是一件很麻烦的工作,而STL的出现很好地解决了这个问题。
我们简单来了解一下STL。STL提供了三种类型的组件:容器、迭代器和算法。容器主要有两类:顺序容器和关联容器。顺序容器(vector、list、deque和string等)是一系列元素的有序集合。关联容器(set、multiset、map和multimap)包括查找元素的键值。迭代器的作用是遍历容器。STL算法库包含四类算法:排序算法、不可变序算法、变序性算法和数值算法。下面就简单介绍一下STL里常用的容器。
1. vector向量容器
vector向量容器不但像数组一样对元素进行随机访问,还能在尾部插入元素,是一种简单、高效的容器,完全可以替代数组。由于vector具有内存自动管理的功能,对于元素的插入和删除,可动态调整所占的内存空间,因此使用时不需要考虑释放空间的问题。使用vector向量容器时,需要包含头文件“vector”。(即#include <vector>)对于vector容器的容量定义,可以事先定义一个固定大小,事后可以随时调整其大小;(例如vector<int> v(10); //定义一个用来存储10个int类型元素的向量容器)也可以事先不用定义其大小,使用push_back()方法从尾部扩张元素,也可以使用insert()在某个元素位置前插入新元素。
vector容器有两个重要的方法,begin()和end()。begin()返回的是首元素位置的迭代器;end()返回的是最后一个元素的下一个元素位置的迭代器。通常在遍历vector所有元素时会用到这两个方法。例如:
vector<int> v(3); vector<int>::iterator it; for(it=v.begin(); it!=v.end(); it++){...}
至于vector元素的删除,调用erase()方法可以删除vector中迭代器所指的一个元素或一段区间中的所有元素,clear()方法则可以删除vector中所有元素。
通过使用size()方法可以返回向量的大小,即元素个数,调用empty()方法返回向量是否为空。
再简单看一下在vector中常用到的算法。使用reverse()反向排列算法,需要定义头文件“#include <algorithm>”,reverse()算法可将向量中某段迭代器区间元素反向排列;使用sort算法可以对向量排序,默认情况下对元素进行升序排列,也可以自己设计排序比较函数,具体使用细节就不赘述了。
2. string基本字符系列容器
C语言中对于字符串只能使用字符数组来处理,显得十分不方便。C++ STL提供了string基本字符序列容器来处理字符串,可以把string理解为字符串类,它提供了添加、删除、替换、查找和比较等丰富方法。使用string容器需要包含头文件声明“#include <string>”。
3. set集合容器
set集合容器实现了红黑树的平衡二叉检索树的数据结构,在插入元素时,它会自动调整二叉树的排列,把该元素放到适当的位置,以确保每个子树根节点的键值大于左子树所有节点的键值,而小于右子树所有节点的键值;另外,还得确保根节点左子树的高度与右子树的高度相等,因为二叉树高度最小,检索速度最快。这里要注意的是,set容器不会插入相同键值的元素。
平衡二叉检索树的检索使用中序遍历算法,检索效率高于vector、deque和list等容器。另外,采用中序遍历算法可将键值由小到大遍历出来,所以,可以理解为平衡二叉检索树在插入元素时,就会自动将元素按键值由小到大的顺序排列。由于构造set集合的主要目的就是为了快速检索,对于set容器中的键值,不可直接去修改。multiset、map和multimap的内部结构也是平衡二叉检索树。
4. multiset多重集合容器
multiset与set一样,也是使用红黑树来组织元素数据的,唯一不同的是,multiset允许重复的元素键值插入,而set则不允许。multiset也需要声明头文件包含“#include <set>”,由于它包含重复元素,所以在插入元素、删除元素、查找元素上较set有差别。
5. map映照容器
map映照容器的元素数据是由一个键值和一个映照数据组成的,键值与映照数据之间具有一一映照的关系。map映照容器的数据结构也是采用红黑树来实现的,插入元素的键值不允许重复,比较函数只对元素的键值进行比较,元素的各项数据可通过键值检索出来。由于map与set采用的都是红黑树的数据结构,所以它们的用法基本类似。使用map容器需要头文件包含语句“#include <map>”。
6. multiset多重映照容器
multiset与map基本相同,唯独不同的是,mulitiset允许插入重复键值的元素。由于允许重复键值存在,所以multiset的元素插入、删除、查找都与map不相同。
7. deque双端队列容器
deque双端队列容器与vector一样,采用线性表顺序存储结构。但与vector唯一不同的是,deque采用分块的线性存储结构来存储数据,每块的大小一般为512字节,称为一个deque块,所有的deque块使用一个Map块进行管理,每个Map数据项纪录每个deque块的首地址。这样,deque块在头部和尾部都可插入和删除元素,而不需移动其他元素。一般来说,当考虑到容器元素的内存分配策略和操作的性能时,deque相对于vector会更有优势。使用deque需要声明头文件包含“#include <deque>”。
8. list双向链表容器
list容器实现了双向链表的数据结构,数据元素是通过链表指针串连成逻辑意义上的线性表,这样,对链表的任一位置的元素进行插入、删除和查找都是极快速的。由于list对象的节点并不要求在一段连续的内存中,所以对于迭代器,只能通过“++”或“--”的操作将迭代器移动到后继/前驱节点元素处。而不能对迭代器进行+n或-n的操作,这点是与vector等不同的地方。使用list需要声明头文件包含“#include <list>”。
9. stack堆栈容器
stack堆栈是一个后进先出的线性表,插入和删除元素都只能在表的一端进行。插入元素的一端称为栈顶,另一端称为栈底。插入元素叫入栈,元素的删除称为出栈。要使用stack必须声明头文件包含语句“#include <stack>”。
10. queue队列容器
queue队列容器是一个先进先出的线性存储表,元素的插入只能在队尾,元素的删除只能在队首。使用queue需要声明头文件包含语句“#include <queue>” 。