STL常用容器及算法介绍

STL常用容器及算法介绍_第1张图片

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 函数

使用数组元素默认的大小比较运算符进行排序,将原数组从小到大重新排列。无返回值。

基本使用方法如下:

sort ( a , a+n )   

数组操作,其中a为数组名,n为数组元素个数

sort ( v.begin(),v.end() )

不定长数组vector操作

lower_bound 函数

查找“大于或等于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";
}

测试结果如下,可以完美运行:

STL常用容器及算法介绍_第2张图片STL常用容器及算法介绍_第3张图片

二、不定长数组 vector

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个堆),有以下四种操作:

  1. 把a和b上方的木块全部归位,把a摞在b上面。
  2. a上方木块全部归位,把a放在b所在木块堆顶部。
  3. b上方木块全部归位,把a及以上木块全部放在b上面。
  4. 把a及以上放在b木块堆上面。

由于在操作过程中每一堆木块的高度是不确定的,而且需要频繁变化移动,可以让每一堆设置为一个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

接着可以自行实现主函数、操作的判断和输出等。

三、集合 set

除数组和不定长数组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类型的迭代器。迭代器使用类似于指针。

四、映射 map

映射就是一个“高阶版”的数组——数组通过 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;

五、栈 stack

栈,即一种后进先出的数据结构(Last in first out-LIFO),有 PUSH和 POP 两种操作。可以理解为一个竖状容器,只能存放单列物品。PUSH 表示为把元素压入栈顶,POP 表示从栈顶把元素弹出(删除元素)。“栈”只有栈顶元素可以活动。

使用栈需要头文件 ,可以用 stack<数据类型> 栈名 的方式声明(这种声明方式可想而知stack本身也是一个模板类)。常用操作如下:

//常用操作:
//定义(声明)
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

六、队列 queue

符合“先进先出”(First In First Out-FIFO),是一个横向的容器,一个口进一个口出,元素(们)整体从入口向出口移动。可以理解为一列士兵走过隧道。

定义在头文件中,声明方式同理为 queue<数据类型> 队列名。常用操作:

queue s;
s.push(x);          //元素入队
s.pop();            //元素出队
int var=s.front();  //取队首元素(但不删除)

七、优先队列 priority_queue

抽象数据类型(ADT),不同于普通队列(又叫“公平队列”),优先队列中优先级高的元素可以先出队列。

优先队列也定义在头文件中,声明方式为 priority_queue<数据类型> 优先队列名。例如一个数据类型为int的优先队列,即表示元素数据越小优先级越低——先出列的是大的整数。因此出队方法由front()变成了top(),即使用pq.top()可以取出队首元素的值(但不删除)。

自定义类型必须设置优先级后可以声明优先队列(和sort一样,只要定义了'<'运算符即可)。如“越小的整数优先级越大的优先队列”可以写成:

priority_queue,greater > pq;
//最后两个< <不要写在一起,否则会被编译器误读为流运算符

该定义方式的原型为:(第三个内容为比较方式)

priority_queue,cmp> pq

  通过头文件定义的 priority_queue pq ,常用的“越小的整数优先级越高”的队列可以声明为 priority_queue,greater> 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

进行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有时会成为性能瓶颈。

你可能感兴趣的:(Henry学C++,Henry的ACM学习笔记,大数据,蓝桥杯,c++,算法,数据结构)