STL介绍
定义
- STL是c++的标准模板库,英文名为Standard Template Library,这个库不需要额外安装,无论哪个版本的c++都可以直接使用,STL极大程度的方便了程序员在此对它进行初步的介绍
STL特点
-
STL中第二个单词即是Template,作为c++中的一个关键字,最简单的使用方法为:
template <class T>
-
在这个声明过后T就可以作为一个虚拟的类/类型使用,例如:
-
而所有STL中的类,都支持模板,也就是说所有你可以使用的STL类中都可以自定义一个类作为模板,个人习惯用node命名自定义的类
容器(Container)
-
这里的容器简单来讲是STL中的各种模板类,比如vector、queue、stack这些都是容器
定义:在数据存储上,有一种对象类型,它可以持有其它对象或指向其它对像的指针,这个类型就是容器
-
容器有三种:
- 序列式容器
vector deque list
- 关联式容器
set multiset map multimap
- 容器适配器
stack queue priority_queue
下表列出STL 定义的三类容器所包含的具体容器类:(以下来自 博客)
顺序性容器
vector: 从后面快速的插入与删除,直接访问任何元素
deque: 从前面或后面快速的插入与删除,直接访问任何元素
list: 双链表,从任何地方快速插入与删除
关联容器
set: 快速查找,不允许重复值
multiset: 快速查找,允许重复值
map: 一对多映射,基于关键字快速查找,不允许重复值
multimap: 一对多映射,基于关键字快速查找,允许重复值
容器适配器
stack: 后进先出
queue: 先进先出
priority_queue: 最高优先级元素总是第一个出列
向量 vector :
是一个线性顺序结构。相当于数组,但其大小可以不预先指定,并且自动扩展。它可以像数组一样被操作,由于它的特性我们完全可以将vector 看作动态数组。
在创建一个vector 后,它会自动在内存中分配一块连续的内存空间进行数据存储,初始的空间大小可以预先指定也可以由vector 默认指定,这个大小即capacity ()函数的返回值。当存储的数据超过分配的空间时vector 会重新分配一块内存块,但这样的分配是很耗时的,在重新分配空间时它会做这样的动作:首先,vector 会申请一块更大的内存块;
然后,将原来的数据拷贝到新的内存块中;
其次,销毁掉原内存块中的对象(调用对象的析构函数);
最后,将原来的内存空间释放掉。
如果vector 保存的数据量很大时,这样的操作一定会导致糟糕的性能(这也是vector 被设计成比较容易拷贝的值类型的原因)。所以说vector 不是在什么情况下性能都好,只有在预先知道它大小的情况下vector 的性能才是最优的。
vector 的特点:
(1) 指定一块如同数组一样的连续存储,但空间可以动态扩展。即它可以像数组一样操作,并且可以进行动态操作。通常体现在push_back() pop_back() 。
(2) 随机访问方便,它像数组一样被访问,即支持[ ] 操作符和vector.at()
(3) 节省空间,因为它是连续存储,在存储数据的区域都是没有被浪费的,但是要明确一点vector 大多情况下并不是满存的,在未存储的区域实际是浪费的。(4) 在内部进行插入、删除操作效率非常低,这样的操作基本上是被禁止的。Vector 被设计成只能在后端进行追加和删除操作,其原因是vector 内部的实现是按照顺序表的原理。
(5) 只能在vector 的最后进行push 和pop ,不能在vector 的头进行push 和pop 。
(6) 当动态添加的数据超过vector 默认分配的大小时要进行内存的重新分配、拷贝与释放,这个操作非常消耗性能。 所以要vector 达到最优的性能,最好在创建vector 时就指定其空间大小。双向链表list
是一个线性链表结构,它的数据由若干个节点构成,每一个节点都包括一个信息块(即实际存储的数据)、一个前驱指针和一个后驱指针。它无需分配指定的内存大小且可以任意伸缩,这是因为它存储在非连续的内存空间中,并且由指针将有序的元素链接起来。
由于其结构的原因,list 随机检索的性能非常的不好,因为它不像vector 那样直接找到元素的地址,而是要从头一个一个的顺序查找,这样目标元素越靠后,它的检索时间就越长。检索时间与目标元素的位置成正比。
虽然随机检索的速度不够快,但是它可以迅速地在任何节点进行插入和删除操作。因为list 的每个节点保存着它在链表中的位置,插入或删除一个元素仅对最多三个元素有所影响,不像vector 会对操作点之后的所有元素的存储地址都有所影响,这一点是vector 不可比拟的。
list 的特点:
(1) 不使用连续的内存空间这样可以随意地进行动态操作;
(2) 可以在内部任何位置快速地插入或删除,当然也可以在两端进行push 和pop 。
(3) 不能进行内部的随机访问,即不支持[ ] 操作符和vector.at() ;
(4) 相对于verctor 占用更多的内存。双端队列deque
是一种优化了的、对序列两端元素进行添加和删除操作的基本序列容器。它允许较为快速地随机访问,但它不像vector 把所有的对象保存在一块连续的内存块,而是采用多个连续的存储块,并且在一个映射结构中保存对这些块及其顺序的跟踪。向deque 两端添加或删除元素的开销很小。它不需要重新分配空间,所以向末端增加元素比vector 更有效。实际上,deque 是对vector 和list 优缺点的结合,它是处于两者之间的一种容器。
deque 的特点:
(1) 随机访问方便,即支持[ ] 操作符和vector.at() ,但性能没有vector 好;
(2) 可以在内部进行插入和删除操作,但性能不及list ;
(3) 可以在两端进行push 、pop ;三者的比较
下图描述了vector 、list 、deque 在内存结构上的特点:
vector 是一段连续的内存块,而deque 是多个连续的内存块, list 是所有数据元素分开保存,可以是任何两个元素没有连续。
vector 的查询性能最好,并且在末端增加数据也很好,除非它重新申请内存段;适合高效地随机存储。
list 是一个链表,任何一个元素都可以是不连续的,但它都有两个指向上一元素和下一元素的指针。所以它对插入、删除元素性能是最好的,而查询性能非常差;适合 大量地插入和删除操作而不关心随机存取的需求。
deque 是介于两者之间,它兼顾了数组和链表的优点,它是分块的链表和多个数组的联合。所以它有被list 好的查询性能,有被vector 好的插入、删除性能。 如果你需要随即存取又关心两端数据的插入和删除,那么deque 是最佳之选。
关联容器
set, multiset, map, multimap 是一种非线性的树结构,具体的说采用的是一种比较高效的特殊的平衡检索二叉树—— 红黑树结构。(至于什么是红黑树,我也不太理解,只能理解到它是一种二叉树结构)
因为关联容器的这四种容器类都使用同一原理,所以他们核心的算法是一致的,但是它们在应用上又有一些差别,先描述一下它们之间的差别。
set ,又称集合,实际上就是一组元素的集合,但其中所包含的元素的值是唯一的,且是按一定顺序排列的,集合中的每个元素被称作集合中的实例。因为其内部是通过链表的方式来组织,所以在插入的时候比vector 快,但在查找和末尾添加上被vector 慢。
multiset ,是多重集合,其实现方式和set 是相似的,只是它不要求集合中的元素是唯一的,也就是说集合中的同一个元素可以出现多次。
map ,提供一种“键- 值”关系的一对一的数据存储能力。其“键”在容器中不可重复,且按一定顺序排列(其实我们可以将set 也看成是一种键- 值关系的存储,只是它只有键没有值。它是map 的一种特殊形式)。由于其是按链表的方式存储,它也继承了链表的优缺点。
multimap , 和map 的原理基本相似,它允许“键”在容器中可以不唯一。
关联容器的特点是明显的,相对于顺序容器,有以下几个主要特点:
1, 其内部实现是采用非线性的二叉树结构,具体的说是红黑树的结构原理实现的;
2, set 和map 保证了元素的唯一性,mulset 和mulmap 扩展了这一属性,可以允许元素不唯一;
3, 元素是有序的集合,默认在插入的时候按升序排列。
基于以上特点,
1, 关联容器对元素的插入和删除操作比vector 要快,因为vector 是顺序存储,而关联容器是链式存储;比list 要慢,是因为即使它们同是链式结构,但list 是线性的,而关联容器是二叉树结构,其改变一个元素涉及到其它元素的变动比list 要多,并且它是排序的,每次插入和删除都需要对元素重新排序;
2, 关联容器对元素的检索操作比vector 慢,但是比list 要快很多。vector 是顺序的连续存储,当然是比不上的,但相对链式的list 要快很多是因为list 是逐个搜索,它搜索的时间是跟容器的大小成正比,而关联容器 查找的复杂度基本是Log(N) ,比如如果有1000 个记录,最多查找10 次,1,000,000 个记录,最多查找20 次。容器越大,关联容器相对list 的优越性就越能体现;
3, 在使用上set 区别于vector,deque,list 的最大特点就是set 是内部排序的,这在查询上虽然逊色于vector ,但是却大大的强于list 。
4, 在使用上map 的功能是不可取代的,它保存了“键- 值”关系的数据,而这种键值关系采用了类数组的方式。数组是用数字类型的下标来索引元素的位置,而map 是用字符型关键字来索引元素的位置。在使用上map 也提供了一种类数组操作的方式,即它可以通过下标来检索数据,这是其他容器做不到的,当然也包括set 。(STL 中只有vector 和map 可以通过类数组的方式操作元素,即如同ele1 方式)
容器适配器
STL 中包含三种适配器:栈stack 、队列queue 和优先级priority_queue 。
适配器是容器的接口,它本身不能直接保存元素,它保存元素的机制是调用另一种顺序容器去实现,即可以把适配器看作“它保存一个容器,这个容器再保存所有元素”。
STL 中提供的三种适配器可以由某一种顺序容器去实现。默认下stack 和queue 基于deque 容器实现,priority_queue 则基于vector 容器实现。当然在创建一个适配器时也可以指定具体的实现容器,创建适配器时在第二个参数上指定具体的顺序容器可以覆盖适配器的默认实现。
由于适配器的特点,一个适配器不是可以由任一个顺序容器都可以实现的。
栈stack 的特点是后进先出,所以它关联的基本容器可以是任意一种顺序容器,因为这些容器类型结构都可以提供栈的操作有求,它们都提供了push_back 、pop_back 和back 操作;
队列queue 的特点是先进先出,适配器要求其关联的基础容器必须提供pop_front 操作,因此其不能建立在vector 容器上;
优先级队列priority_queue 适配器要求提供随机访问功能,因此不能建立在list 容器上
- 序列式容器
迭代器(Iterator)
-
迭代器简单理解就是stl中遍历用到的指针
定义:Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示
例如普通的遍历一个数组:
int a[n]; for (int i = 0; i < n; ++i) { ; }
这其中i就可以说是迭代器的前身
stl中最简单的遍历容器的方法是
vector<int> x; for (auto i : x) { //c++11新特性auto ; }
这其中i就是一个迭代器,它与指针不完全相同,但是起到了指针的作用,还记得容器的定义吗,容器持有其它对象或指向其它对像的指针,这个指针的对外接口就是迭代器。
-
迭代器的使用
iterator是一种容器的内置类型,它需要在容器的基础上进行使用,如
vector<int>::iterator iter;
begin(), end()两个方法返回了STL中的各个容器的首尾迭代器(左闭右开)
简单使用:
vector<int> x; for (auto i = x.begin(); i != x.end(); ++i) { ; }
auto
-
示例代码中用到了auto我觉得有必要解释一下。为了方便容器的使用,c++11中提供了auto这个新特性,即自动适应迭代器的类型
vector<int> x; for (auto i = x.begin(); i != x.end(); ++i) { ; }
代码中auto自动适应了vector<int>的迭代器vector<int>::iterator
算法函数
-
全部包含在algorithm头文件中
-
STL中提供了大量方便使用且常用的算法(一手sort吹爆)
-
STL中的算法函数强大不仅仅是在效率上,而且在于它可以自动适配不同的容器
-
两个非常常用的函数
-
查找函数
_FIter lower_bound(_FIter, _FIter, const _Tp&);
_FIter lower_bound(_FIter, _FIter, const _Tp&, _Compare);返回值为小于等于某个值的最小迭代
参数_FIter就是迭代器,引用类型_Tp是你查找的值,_Compare是函数指针表示你的比较函数
这个函数过于强大以至于数组也可以用这个函数进行查找,内部实现是简单的二分查找
给出两个例子:
int need = 5; int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int pos = lower_bound(a, a + 10, need) - a;
vector<int>a; int need = 5; for (int i = 0; i < 10; ++i) a.push_back(i); int pos = lower_bound(a.begin(), a.end(), need) - a.begin();
由于返回的是迭代器一般需要的是数组或容器中的相对位置所以最后一般会做指针的减法
Compare函数需要是两个参数分别代表前一个数和后一个数与参数的前后顺序一致,返回是否前面小于后面,例:
bool qwq(int a, int b) { return a > b; } int main(int argc, char *argv[]) { int need = 5; int a[] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}; int pos = lower_bound(a, a + 10, need, qwq) - a; printf("%d", pos); return 0; }
无Compare函数会默认调用这个类的小于运算符函数。
注意使用这个函数的先决条件是有序,有序包含了有比较函数和按比较函数已完全排序两个层面。
-
排序函数
void sort(_RAIter, _RAIter);
void sort(_RAIter, _RAIter, _Compare);sort使用极广,效率和适用范围过于优秀以至于我甚至不知道各种排序的具体过程(STL重度依赖症了解一下)
参数和查找函数完全一致不再写范例。
这个函数可默认时间复杂度为nlog(n)
-
-
接下来列举其他的算法函数
inline void reverse(_BidirectionalIterator __first, _BidirectionalIterator __last) 使容器反向
_EuclideanRingElement __gcd(_EuclideanRingElement __m, _EuclideanRingElement __n) 求最大公因数
bool next_permutation(_BIter, _BIter); 求下一个排列,未到达排列尾返回true
_FIter max_element(_FIter, _FIter, _Compare); 求第一个最大值 返回迭代 compare函数可省
_FIter min_element(_FIter, _FIter, _Compare); 求第一个最小值 返回迭代 compare函数可省
_FIter unique(_FIter, _FIter, _BinaryPredicate); 去重,需要有序