STL指的是C++标准模板库(Standard Template Library),本文介绍常用算法及容器。
具体可参考官方文档:C++ Standard Library - cppreference.com
目录
一、排序与检索
sort 函数
lower_bound 函数
典型样例
二、不定长数组 vector
三、集合 set
四、映射 map
五、栈 stack
六、队列 queue
七、优先队列 priority_queue
八、栈和队列进阶使用(再谈栈和队列)
九、链表
十、测试STL
常用排序可以使用 algorithm 头文件中的 sort 和 lower_bound 函数。
使用数组元素默认的大小比较运算符进行排序,将原数组从小到大重新排列。无返回值。
基本使用方法如下:
sort ( a , a+n )
数组操作,其中a为数组名,n为数组元素个数
sort ( v.begin(),v.end() )
不定长数组vector操作
查找“大于或等于x的第一个位置”。返回元素位置。基本使用方法如下:
lower_bound ( a , a+n , x )
数组操作,其中a为数组名,n为数组元素个数,x为待查找元素
vector操作同理
注意 lower_bound 不是精确查找,而是寻找大于等于的第一个。返回为 int* 型的元素位置,如果要定位到精确的元素,需要减去首元素位置:
int t = lower_bound ( a , a+n , x ) - a ;
当使用 lower_bound 进行查找时,使用变量 t 储存返回值减去首元素位置(数组名)的结果,再将 a[t] 与 x 进行比较,如果两者相同,说明找到且下标为 t(即原排列的第t+1个);如果不相等,说明原数组中不存在x。
输入5个数字作为卡片数值,从小到大排列并找出输入的具体数在哪一张卡片上。
#include
#include //sort和lower_bound头文件
using namespace std;
const int ArrayNumber = 5;
int main() {
int a[ArrayNumber], x;
for (int i = 0; i < ArrayNumber; i++) {
cin >> a[i];
}
cin >> x;
//到此数据全部输入完毕,首先使用sort进行排序
sort(a, a + ArrayNumber);
//接着使用lower_bound进行查找
int t = lower_bound(a, a + ArrayNumber, x)-a;
//判断是否存在
if (a[t] == x)
cout << x << " found at " << t + 1 << endl;
else
cout << "not found\n";
}
测试结果如下,可以完美运行:
vector本质上是一个模板类,不但可以储存不定长的数据(数据成员),还封装了一些常用操作函数(成员函数)。常用操作如下:
a.size() | 读取大小 |
a.resize(n) | 改变大小为n(下标≥n的都清除) |
a.push_back(x) | 将x添加在a尾部(size会+1) |
a.pop_back() | 删除最后一个元素(size会-1) |
a.empty() | 测试是否为空 |
由于vector是模板类,所以需要带上数据类型一起声明:
vector <数据类型> 数组名
如:vector
a; vector b;
vector的典型使用是移木块问题(The Blocks Problem,UVa 101),题目的大概意思是从左到右有n的木块(n个堆),有以下四种操作:
由于在操作过程中每一堆木块的高度是不确定的,而且需要频繁变化移动,可以让每一堆设置为一个vector:
const int maxn=30; //堆数的最大长度
vector pile[maxn];
//定义了一个vector数组,外围数组的每一个元素都是一个vector
接着介绍几个本程序中重要的操作函数。第一个函数是找出指定木块a所在的堆数和高度层数:
void find_block(int a,int& p,int& h) //以引用的方式传参
{
for(p=0;p
第二个函数执行的是把第p堆高度为h的木块上方的所有木块移回原位:
void clear_above(int p,int h)
{
for(int i=h+1;i
第三个函数是把第p堆高度为h及其以上的木块整体移动到p2堆的顶部:
void pile_onto(int p,int h,int p2)
{
for(int i=h;i
接着可以自行实现主函数、操作的判断和输出等。
除数组和不定长数组vector,集合set也是常见容器。区别于数组,set中每个元素只可以出现一次,且元素会自动排序(如果储存数据则按照大小排序,储存字符串则按照字典排序)。利用自动排序功能可以实现字典统计等功能。
如经典例题 安迪的第一个字典(Andy's First Dictionary,UVa 10815)中,需要对一段具体的英文文本统计字典。首先可以定义一个 string 类型的集合 dict:
set dict; //string集合
接着可以将整个文本读取到一个字符串中,遍历字符串并使用 isalpha() 函数判断是否为英文字符(因为标点符号不能读入作为字典),如果是则使用 tolower() 函数全部转成小写,如果不是则变成空格。这部分核心代码如下:
//设存放文本的字符串为s
for(int i=0;i>s.length();i++){
if(isalpha(s[i]))
s[i]=tolower(s[i]);
else
s[i]=' ';
}
然后使用字符串流 stringstream 模拟读入,并将读入的单词word(字符串) dict.insert(word) 插入集合 dict 中。最后遍历输出,需要使用 iterator迭代器:
for(set::iterator it=dict.begin();it!=dict.end();++it)
cout<<*it<<'\n';
由于 set.begin() 和 set.end() 返回值都是迭代器(都是仿照迭代器命名的),所以这里set
映射就是一个“高阶版”的数组——数组通过 int 型的下标定位元素,而map可以使用各种数据类型定位元素:
map
cnt 表示的是该映射是从字符串string映射到int型,相当于一个int型的数组使用string进行元素定位。
map和set都需要自己名称的头文件,且两者都支持 insert / find / count / remove 操作。其中 count函数表示对元素进行计数,如果在容器中存在则返回1,如果不存在则返回0。
如一个 map
cnt,需要添加一个string为s的元素为a 可以直接写:cnt [s]=a;
栈,即一种后进先出的数据结构(Last in first out-LIFO),有 PUSH和 POP 两种操作。可以理解为一个竖状容器,只能存放单列物品。PUSH 表示为把元素压入栈顶,POP 表示从栈顶把元素弹出(删除元素)。“栈”只有栈顶元素可以活动。
使用栈需要头文件
//常用操作:
//定义(声明)
stack s;
//元素入栈
s.push(a);
//元素出栈(且原栈内删除),没有返回值,整个函数就是一个操作
s.pop();
//取最顶层元素(元素不删除)
int var=s.top();
典型例题有集合栈计算机(The SetStack Computer,ACM/ICPC NWERC 2006,UVa12096),具体题目在书P115~117,思路为定义int集合set到int型id的映射,以及int集合set为数据类型的不定长数组:
typedef set Set;
map IDcache; //把集合映射成ID,通过集合取ID
vector Setcache; //可以根据ID取集合,集合下标即为ID
在主函数中定义栈,使用栈操作即可。
这里要补充两个集合的常用操作函数:取交集与取并集
//首先定义宏
#define ALL(x) x.begin(),x.end() //表示集合中所有的内容
#define INS(x) inserter(x,x.begin()) //表示插入迭代器
//宏很复杂,带参数的宏可以大致理解为“类似于函数的东西”
set_union(ALL(x1),ALL(x2),INS(x)); //取x1x2并集,组合为x
set_intersection(ALL(x1),ALL(x2),INS(x)) //取x1x2交集,结合为x
符合“先进先出”(First In First Out-FIFO),是一个横向的容器,一个口进一个口出,元素(们)整体从入口向出口移动。可以理解为一列士兵走过隧道。
定义在头文件
queue s;
s.push(x); //元素入队
s.pop(); //元素出队
int var=s.front(); //取队首元素(但不删除)
抽象数据类型(ADT),不同于普通队列(又叫“公平队列”),优先队列中优先级高的元素可以先出队列。
优先队列也定义在头文件
自定义类型必须设置优先级后可以声明优先队列(和sort一样,只要定义了'<'运算符即可)。如“越小的整数优先级越大的优先队列”可以写成:
priority_queue,greater > pq;
//最后两个< <不要写在一起,否则会被编译器误读为流运算符
该定义方式的原型为:(第三个内容为比较方式)
priority_queue,cmp> pq
通过
典型例题为 丑数(Ugly Numbers,Uva 136),指的是不能被2,3,5以外的素数整除的数,要求从小到大排列的第1500个丑数。主要思路是如果x是丑数,那么2x,3x,5x都是丑数,所以可以通过一个丑数生成三个新丑数加入队列,同时注意生成的丑数不能已经存在。
“不能已经存在”顾名思义想到使用集合set(集合set不能存在重复元素,可以使用set.count()判断是否已经存在该元素),而“从小到大第1500个”可以想到优先队列——每次出去最小的,出去1500次。因为每一次出去伴随三个新的进来,而且新的肯定比出去的大(2/3/5倍),所以可以确保1500次之后不会出现更小的数。代码很简单,定义队列和集合之后就可以用循环解决:
typedef long long LL;
priority_queue,greater> pq;
set s;
//使用循环即可
从之前的STL中学习了栈和队列的基本概念——栈(stack)是一种后进先出(LIFO)的数据容器,而队列(queue)是一种先进先出(FIFO)的容器,两者都需要头文件且都可以通过push类操作加入元素、top操作显示首部元素的值、pop操作弹出首部元素。当然还有优先队列,优先级高的先出队列。
对于一个队列(不考虑优先队列),新元素插入的位置一定是队列“尾部”,旧元素出列的位置一定是队列的“首部”。但是如果想让新元素插入到“首部”怎么办呢?
参考典型例题 并行程序模拟(Concurrency Simulator,ACM World Finals 1991,Uva210),有两种解决思路:
第一种是放弃STL定义的队列
自己写“支持首部插入”的队列,用两个变量front和rear代表队列当前首尾下标。很容易想传统队列出队是x=q[front++],入队是q[++rear]=x;那么“插入到对首”操作就是q[--front]=x。
不过可能会导致数组越界的错误(比如原front=0)
第二种方法是使用STL中的“双端队列”deque
简单介绍deque的一些成员函数,个人仅作了解:
多用于需要在“队列”两端插入和删除的情况下。
q.front() 返回第一个元素的引用 q.back() 返回最后一个元素的引用 q.push_back(x) 尾部插入元素 q.push_front(x) 首部插入元素 q.pop_back() 移除尾部元素 q.pop_front() 移除首部元素
其实“栈”在计算机中的意义就是在于求表达式,如括号表达式中,左括号和运算部分入栈,当遇到右括号时,左括号及其以上部分出栈运算,再把计算结果重新入栈。
典型例题为 矩阵链乘(Matrix Chain Multiplication,Uva 442),使用了结构体栈看。由于规定了两个矩阵相乘运算就需要加括号,所以括号不需要入栈,只要将数据入栈、遇到一个后括号就出栈两个矩阵(结构体)进行相乘即可。该题较为简单。
之前介绍了不定长数组vector,但是vector的使用非常局限,按照目前的知识只能不断从末尾添加/删除元素,如果想从中间插入元素或者删除元素,则需要将大量数据下标整体移动——通常会导致TLE,此处引入链表(Linked List),顾名思义就是互相之间有联系的列表,但是这种联系不仅仅是下标之间的前后关系,更重要的是本元素中存储了指向下一个元素或前一个元素的指引,那样中间插入元素和删除元素很方便(只需改变指引即可,没必要真正把空间“插入”到中间)。
链表中的“指引”不一定需要指针,也可以是下标——先把数据全部存储在一个数组里,而真正顺序中某个数据的“下一个”可以指向数组中任一下标。
为了方便起见,常常在链表前加入一个虚拟的head节点,那样对第一个元素的操作其实和中间元素的操作可以等效
通常单向链表由数据和指引构成:
//单链表的一个节点
struct chain{
int a; //存储的数据
struct chain* Next=NULL; //指向下一个节点的指针,默认(结尾处)指向NULL
}
可以在结构体中定义一些成员函数以实现一些便捷操作。但是单链表有一个缺陷,就是只能有一个方向(包括只能沿着一个方向遍历)。对于一些复杂的操作,如反转整条链条,单链表肯定是无法胜任,所以引入双链表:
//双链表的一个节点
struct doubleLinkList_chain{
int a;
struct doubleLinkList_chain* Left;
struct doubleLinkList_chain* Right;
}
可以同时指向两个方向,但初始化等操作都会比较麻烦。由于本人在学校荣誉课程已经熟练掌握链表操作,此处链表章节简单带过。初学者可自行查找文档学习~
进行STL库的测试时,有众多技巧。
第一点是使用随机数作为测试数据。由于单个数据不具有代表性,多数情况下需要使用随机数生成测试数据。可以直接使用 cstlib 中的 rand() 生成 [ 0 , RAND_MAX ] 的正整数,但是局限在于正整数生成上界不能大于系统的 RAND_MAX(不同系统不同,至少为32767),可以满足普通程序的测试需要。
int var=rand()%n; //生成[0,n-1]的正整数
此外,在随机数使用前,于主程序中需要进行一次 srand(time(NULL)),目的在于“初始化随机数种子”,因为种子的伪随机数计算的依据,所以调用该行可以使伪随机数每次不一样。注意不能多次调用,否则生成的随机数次次一样!
但是如果需要对一组测试出程序问题的数据进行重现,那么就不需要调用随机数种子。
第二点是vector等容器数据类型形参使用引用,目的是避免不必要的值被复制。
第三点是使用assert宏。用法为 assert ( 表达式 ),表示为“当表达式为真时无变化,当表达式为假时终止程序并给出错误提示”。由于代码简洁且可以知道是哪一行引起,所以经常在测试中被使用。
测试了vector/set/map,发现速度都很快,其中vector接近数组,set 和 map 每次插入、查找和删除时间和元素的个数的对数呈线性关系。
因此,STL有时会成为性能瓶颈。