STL与基本数据结构
文章目录
前言与指北
这一段陆陆续续写了大概半个寒假吧……因为这些东西坑还是挺多的,资料也找了好久,C++ Prime Plus都给我丢桌面上时时刻刻翻了……所以各位大佬轻喷,若有错误直接指出来就行了
顺序式容器
- vector:动态数组,从末尾能快速插入和删除,可由下标直接访问任何元素
- list:双链表,可以从任何地方快速插入与删除任何元素
- deque:双向队列,可以从首部或尾部快速插入与删除,直接访问任何元素
- queue:队列,先进先出(FIFO)
- priority_queue优先列,最高优先级元素总是第一个出列
- stack:栈,后进先出(LIFO)
关联式容器
- set:集合,快速查找,不允许重复值。
- multiset:多重集,快速查找,允许重复值
- map:一对一映射,基于关键字快速查找,不允许重复值
- multimap:一对多映射,基于关键字快速查找,允许重复值
- unordered_set:无序单重集合(C++11)
- unordered_ multiset:无序多重集合(C++11)
- unordered_map:无序单重映射表(C++11)
- unordered_multimap:无序多重映射表(C++11)
其中带unordered的容器版本不支持双向迭代器,但是支持前向迭代器。
由于该版本容器由哈希表实现,导致该容器上的双向遍历被复杂化,似乎没有一种合适的方法能够做到高效快速。故只提供前向迭代器
由于底层实现为哈希表,能够提供更快的查询时间,在标准开放且需要大量查询的场合使用unordered版本容器有奇效。
前向迭代器(Forward Iterator):该类迭代器可以在一个正确的区间中进行读写操作,它拥有单步向前迭代元素的能力。
双向迭代器(Bidirectional Iterator):该类迭代器是在前向迭代器的基础上提供了单步向后迭代元素的能力。例如:list, set, multiset, map, multimap。
也就是说,前向迭代器并不支持这两个操作:
–iter: 向后遍历一步(返回最新的位置)
iter–: 向后遍历一步(返回原有的位置)
1. vector(动态数组)
数组是基本的数据结构,有静态数组和动态数组两种类型。
在算法竞赛中,如果空间足够,能用静态数组就用静态数组,而不用指针管理动态数组,这样编程比较简单并且不会出错;如果空间紧张,可以用STL的 vector建立动态数组,不仅节约空间,而且也不易出错。
vector是STL的动态数组,在运行时能根据需要改变数组大小。它的内存空间是连续的。所以索引可以在常数时间内完成,但是在中间进行插入和删除操作会造成内存块的复制。另外,如果数组后面的内存空间不够,程序会重新申请一块足够大的内存。这些都会影响 vector的效率
但是一些算法竞赛的时候第一个数据通常都是输入数据的数量,这个时候可以用reserve()函数手动申请一块内存来避免反复的扩容。见本小节末尾。
vector容器是一个模板类,能存放任何类型的对象。
1.1 vector的常用定义
例子 | 说明 |
---|---|
vectorarr; | 默认定义,定义的数组arr为空,Type可以是任何常见数据类型,包括结构体和容器。 |
vectorarr(233,666); | 定义arr,其中有233个值为666的元素,每个元素的类型为整型 |
struct Point {int x,int y}; vectornode; | 使用结构体Point来定义动态数组,每个node[n]即为一个结构体成员 |
1.2 vector的常用操作
vector支持C语言数组的所有特性,包括按照下标解引元素。不同的是指针将被替换成迭代器(iterator)。
arr.begin():获取vector的首部位置(第一个元素),返回值为迭代器
arr.end():获取vector的尾部位置(最后一个元素+1),返回值为迭代器
功能 | 例子 |
---|---|
arr.pop_back(); | 从数组尾部删除元素 |
arr.push_back(item); | 从数组尾部添加元素 |
arr.erase(arr.begin()+i,arr.begin()+j); | 删除指定区间的值(两个参数均为迭代器) |
arr.insert(arr.begin()+i ,k); | 在第i个元素前插入值k(第一个参数为迭代器) |
sort(arr.begin(),arr.end(),greater()); | 对指定区间排序(升序排序) |
sort(arr.begin(),arr.end()); sort(arr.begin(),arr.end(),less()); | 对指定区间排序(降序排序,第三个参数缺省默认降序) |
arr.reserve(n); | 指定容器能存储数据的个数,可用arr.capacity();查看 |
arr.resize(n); | 指定有效元素的个数,不改变原有值,新指定的元素置0 例:arr={1,2,3}; arr.resize(5); arr={1,2,3,0,0}; |
arr.clear(); | 清空Vector |
unsigned n=arr.size(); | 统计元素个数赋值给变量n。请注意size的返回值为unsigned类型,不能与负数进行大小比较。 |
unsigned n=arr.capacity(); | 统计容器能存储数据的个数赋值给变量n。返回值为unsigned类型,不能与负数进行大小比较。 |
reverse(arr.begin(),arr.end()); | 翻转指定区间内的迭代器 (1,2,3 —> 3,2,1) |
关于resize与reserve:
当创建空容器时, 容量(capacity)为 0;当用完时,增加原容量的 1/2。适用如 vector这种 元素连续存储的容器, 如为list则不同。
capacity 一般大于size的原因是为了避免每次增加数据时都要重新分配内存,所以一般会生成一个较大的空间,以便随后的数据插入。
也就可以避免大量数据输入导致反复扩容带来的性能下降,在程序运行前预估所需空间进行reserve是一个明智的选择
2. stack(栈)
栈和队列的示意图:
栈(stack)是一种基本的数据结构,特点是“后进先出”(LIFO),即最后插入的元素最先出来。
跟其相对的是队列(queue),即最先插入的元素最先出来。
栈的概念很容易懂,难于使用栈这一结构来编写程序才是难点。本文只简单列出栈支持的操作。
2.1 stack的常用定义
stack容器是一个模板类,能存放任何类型的对象。
例子 | 说明 |
---|---|
stacks; | 定义栈,Type为数据类型,类似于vector。 |
2.2 stack的常用操作
例子 | 功能 |
---|---|
s.push(item); | 把item置入栈顶 |
s.top(); | 返回栈顶的元素 |
s.pop(); | 删除栈顶的元素 |
s.size(); | 返回栈中元素的个数 |
s.empty(); | 检查栈是否为空,为空返回true |
3. queue(队列)
队列的介绍在上一节有提过,队列(queue),是一种基本的数据结构,特点是“先进先出”(FIFO),即最先插入的元素最先出来。就像在食堂打饭,第一个到达窗口的人肯定是第一个打到饭的。队列就是这一类非常常见的数据结构。
3.1 queue的常用定义
queue容器是一个模板类,能存放任何类型的对象。
例子 | 说明 |
---|---|
queueq; | 定义栈,Type为数据类型,类似于vector。 |
3.2 queue的常用操作
例子 | 功能 |
---|---|
q.push(item); | 把item置入队列 |
q.front(); | 返回队首的元素 |
q.pop(); | 删除队首的元素 |
q.back(); | 返回队尾的元素 |
q.size(); | 返回队列中元素的个数 |
q.empty(); | 检查队列是否为空,为空返回true |
4. priority_queue(优先队列)
在一个优先队列中,每一个元素拥有一个优先级,在出队的时候按照最高优先值元素先出队的原则运作,它是队列和排序的完美结合,不仅可以存储数据,还能将这些数据按照指定的规则进行排序。每次的push和pop操作都会动态调整顺序,将优先级高的元素放置在队首。
在STL中,优先队列是用二叉堆来实现的,在队列中push与pop一个元素所需的时间复杂度都是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
3.1 priority_queue的常用定义
priority_queue容器是一个模板类,能存放任何类型的对象。
例子 | 说明 |
---|---|
priority_queueq; | 定义栈,Type为数据类型,类似于vector。 |
3.2 priority_queue的常用操作
在优先队列中,没有 front() 函数与 back() 函数,而只能通过 top() 函数来访问队首元素(也可称为堆顶元素),也就是优先级最高的元素。
例子 | 功能 |
---|---|
q.push(item); | 把item置入队列 |
q.top(); | 返回队首的元素 |
q.pop(); | 删除队首的元素 |
q.size(); | 返回队列中元素的个数 |
q.empty(); | 检查队列是否为空,为空返回true |
3.2.1 基本数据类型的优先级设置
此处指的基本数据类型就是 int 型,double 型,char 型等可以直接使用的数据类型,优先队列对他们的优先级设置一般是数值越大的优先级高**,因此队首元素就是优先队列内值最大的那个(如果是 char 型,则是字典序最大的)。
//下面两种优先队列的定义是等价的
priority_queue<int> q;
priority_queue<int,vector<int>,less<int> >q;//less<int>后面有一个空格
例子之中的less<int>
是STL的另一个重要组成部分仿函数。less<int>
与greater<int>
都属于STL自带的仿函数,若要将数值最小的元素置于队首,则使用greater仿函数:
priority_queue<int,vector<int>,greater<int> >q;
仿函数(functor),就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。
3.2.2结构体的优先级设置
-
重载运算符"<"
在结构体内部重载 ‘<’,改变小于号的功能(例如把他重载为大于号)
struct student { int grade; string name; //重载运算符,grade 值高的优先级大 friend operator<(student s1, student s2) { return s1.grade < s2.grade; } };
-
将重载的函数写在结构体外面,作为参数传给优先队列
struct fruit { string name; int price; }; struct cmp { // "<" 表示 price 大的优先级高 bool operator()(fruit f1, fruit f2) { return f1.price < f2.price; } };
声明完毕只需要使用参数cmp声明优先队列即可。
priority_queue<fruit,vector<fruit>,cmp> q;
5. list(链表)
STL内置的list在数据结构上是一个双向链表。它最大的特点就是内存空间非连续。只能通过指针来进行数据访问,它可以高效的在任意位置插入和删除。且时间复杂度是常数级别的。
list和vector的优缺点正好相反。应用场景也不同。
- vector:插入和删除操作很少,随机访问元素频繁(指下标访问元素)
- list:插入和删除频繁,但是随机访问较少
5.1 list的常用定义
例子 | 说明 |
---|---|
listarr; | 默认定义,定义的链表arr为空,Type可以是任何常见数据类型,包括结构体和容器。 |
5.2 list的常用操作
list不支持使用下标访问元素(随机存取)
功能 | 例子 |
---|---|
arr.pop_back(); | 从数组尾部删除元素 |
arr.push_back(item); | 从数组尾部添加元素 |
arr.erase(arr.begin()+i,arr.begin()+j); | 删除指定区间的值(两个参数均为迭代器) |
arr.insert(arr.begin()+i ,k); | 在第i个元素前插入值k(第一个参数为迭代器) |
arr.reserve(n); | 指定容器能存储数据的个数,可用arr.capacity();查看 |
arr.resize(n); | 指定有效元素的个数,不改变原有值,新指定的元素置0 例:arr={1,2,3}; arr.resize(5); arr={1,2,3,0,0}; |
arr.clear(); | 清空list |
unsigned n=arr.size(); | 统计元素个数赋值给变量n。请注意size的返回值为unsigned类型,不能与负数进行大小比较。 |
unsigned n=arr.capacity(); | 统计容器能存储数据的个数赋值给变量n。返回值为unsigned类型,不能与负数进行大小比较。 |
reverse(arr.begin(),arr.end()); | 翻转指定区间内的迭代器 (1,2,3 —> 3,2,1) |
其实list和vector很多操作都是差不多的,list和vector的底层实现分别是链表和顺序表,操作起来也跟链表和顺序表类似,list不能随机存取,但是vector插入操作很费时
6. set & multiset(集合与多重集)
set的概念很简单,跟数学内的集合定义相仿。STL的set使用二叉搜索树实现。集合中的元素有序且唯一。访问元素的复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),十分高效。
multiset可以直接理解为存在相同元素的set(多重集)。
6.1 set & multiset的常用定义
例子 | 说明 |
---|---|
sets; | 定义集合,Type为数据类型。 |
multisets; | 定义多重集,Type为数据类型。 |
6.2 set & multiset的常用操作
set & multiset的常用操作相仿,也不方便展开叙述,故简单列出。
非常建议各位单独去找一下该容器的资料,因为它们实在是一下讲不完
例子 | 功能 |
---|---|
s.insert(item) | 将元素置入集合 |
s.size(); | 返回队列中元素的个数 |
s.empty(); | 检查队列是否为空,为空返回true |
s.count(x); | 返回容器中元素x的个数 |
s.lower_bound(k); | 返回第一个不小于键参数k的元素的迭代器 |
s.upper_bound(k); | 返回最后一个大于键参数k的元素的迭代器 |
7. map & multimap(一对一,一对多映射)
如果说set对应数学中的“集合”,那么map对应的就是“映射”。map是一种key-value型容器,其中key是关键字,起到索引作用,而value就是其对应的值。与set不同的是它支持下标访问。
map的概念很简单,跟数学内的集合定义相仿。STL的map使用平衡二叉树实现。
multiset可以直接理解为存在一对多映射的map。
7.1 set & multiset的常用定义
例子 | 说明 |
---|---|
mapm; | 定义集合,Type为数据类型。 |
multimapm; | 定义多重集,Type为数据类型。 |
7.2 set & multiset的常用操作
map & multimap的常用操作相仿,也不方便展开叙述,故简单列出。
map使用起来有一点特殊,先来看个例子:
-
定义:
map<string,int>stu;
-
赋值:
stu["Ansel"]=233;
在此处的字符串“Ansel”会被当做普通数组的下标来使用 -
查找:当需要输出字符串“Ansel”所对应的键值的时候,直接进行如下操作即可:
cout<<stu["Ansel"];
控制台输出如下:233
非常建议各位单独去找一下该容器的资料,因为它们实在是一下讲不完
例子 | 功能 |
---|---|
m.insert(item) | 将元素置入集合 |
m.insert(make_pair(key,val)) | 插入一个元素(必须以pair形式插入) |
m.erase(it) | 删除迭代器it所指向的元素 |
m.erase(key) | 删除键值为key的元素 |
m.size(); | 返回队列中元素的个数 |
m.empty(); | 检查队列是否为空,为空返回true |
m.count(x); | 返回容器中元素x的个数 |
m.lower_bound(k); | 返回第一个不小于键参数k的元素的迭代器 |
m.upper_bound(k); | 返回最后一个大于键参数k的元素的迭代器 |
后记与瞎扯
这篇文章写了一年(真的),期间有想过咕,不过我还是把它写出来了,主要是STL在竞赛中用起来是真的舒服,但是我自己有的时候会 放 飞 自 我 导致代码疯狂报错,所以写这个也算是复习?……就这样吧。有不对劲的请大家赶快指正,我三月打完蓝桥杯就学JAVA去……