STL整理
1. 主要组件
-
容器(Containers):各种数据结构,如Vector,List,Deque,Set,Map,用来存放数据,STL容器是一种Class Template。
-
算法(Algorithms):各种常用算法如Sort,Search,Copy,Erase,从实现的角度来看,STL算法是一种Function Templates。
-
迭代器(Iterators):扮演容器与算法之间的胶合剂,是所谓的“泛型指针”,共有五种类型,以及其它衍生变化,从实现的角度来看,迭代器是一种将:Operators*,Operator->,Operator++,Operator--等相关操作予以重载的Class Template。所有STL容器都附带有自己专属的迭代器——是的,只有容器设计者才知道如何遍历自己的元素,原生指针(Native pointer)也是一种迭代器。
-
仿函数(Functors):行为类似函数,可作为算法的某种策略(Policy),从实现的角度来看,仿函数是一种重载了Operator()的Class 或 Class Template。一般函数指针可视为狭义的仿函数。
-
适配器(配接器)(Adapters):一种用来修饰容器(Containers)或仿函数(Functors)或迭代器(Iterators)接口的东西,例如:STL提供的Queue和Stack,虽然看似容器,其实只能算是一种容器配接器,因为 它们的底部完全借助Deque,所有操作有底层的Deque供应。改变Functor接口者,称为Function Adapter;改变Container接口者,称为Container Adapter;改变Iterator接口者,称为Iterator Adapter。
-
分配器(Allocators):负责空间配置与管理,从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的Class Template。
2.Vector原理
-
数据安排及操作方式与array非常相似。两者的唯一差别在于空间运用的灵活性。
-
静态空间,一旦配置好了就不能改变了,如果程序需要一个更大的array,只能自己再申请一个更大的array,然后将以前的array中的内容全部拷贝到新的array中。
-
动态开辟的空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新的元素。
-
在增加元素时,如果超过自身最大的容量,vector则将自身的容量扩充为原来的两倍。扩充空间需要经过的步骤:重新配置空间,元素移动,释放旧的内存空间。一旦vector空间重新配置,则指向原来vector的所有迭代器都失效了,因为vector的地址改变了。
-
vector为什么要用加倍扩容而不是每次增加一个固定的扩容容量?
vector在push_back以成倍增长可以在均摊后达到O(1)的事件复杂度,相对于增长指定大小的O(n)时间复杂度更好。
为了防止申请内存的浪费,现在使用较多的有2倍与1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用。
- vector的扩容方式为什么是1.5倍或2倍?
-
假如说我们是以2倍方式扩容(1,2,4,8,16),则第i次扩容期间所需要的空间总量就是2^i次方,如果第4次扩容时总共需要8个元素大小的空间,但是前3次已经释放的空间加起来的总量,刚好是7,而7小于8,不足以我们第4次扩容时所需要的空间,也就是说,如果恰巧以2倍方式扩容,那么每次扩容时前面释放的空间它都不足以支持本次的扩容!!!那么如果是以更高倍数的方式进行扩容,则这个空间它的浪费情况就会更高!!!也就是说,以2倍或者更高倍数的方式进行扩容,会存在2个问题:
-
空间浪费可能比较大
-
无法使用前面已经释放的空间
-
-
STL标准没有严格说明我们应该按照哪一种方式进行扩容,因此不同STL的实现厂商都是按照自己的方式进行扩容的。
-
一般情况下,在Windows的VS系列编译器下,是按照1.5倍的方式进行扩容,在Linux的g++中,是按照2倍的方式进行扩容的。
-
size、resize、reserve、capacity的区别
size表示当前vector中有多少个元素(即finish – start);当前容器所存储的个数,
resize可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。可以有多个参数。创建指定数量的元素并指定vector的存储空间。既分配空间又创建对象。
reserve是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),从而达到提高效率的目的,其次还可以减少多次要拷贝数据的问题。reserve它只是保证vector中的空间大小(capacity)最少达到参数所指定的大小n。并且它只有一个参数。指定vector的元素总数,不创建对象。
capacity函数表示它已经分配的内存中可以容纳多少元素(即end_of_storage – start)。即容器在分配新的存储空间能存储的元素总数。返回vector中能存储元素的最大数。
- vector可能导致其迭代器失效的操作有哪些?
resize、reserve、insert、assign、push_back等会引起其底层空间改变的操作,都有可能使迭代器失效。
指定位置元素的删除操作--erase。
- push_back和emplace_back的区别?
emplace_back() 和 push_back() 的主要区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
3.List原理
-
list的底层是一个双向链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。
-
和vector容器迭代器的实现方式不同,由于 list 容器的元素并不是连续存储的,所以该容器迭代器中,必须包含一个可以指向 list 容器的指针,并且该指针还可以借助重载的 *、++、--、==、!= 等运算符,实现迭代器正确的递增、递减、取值等操作。
- 常见时间复杂度
vector插入、查找、删除时间复杂度分别为:O(n)、O(1)、O(n);
list插入、查找、删除时间复杂度分别为:O(1)、O(n)、O(1)。
4. deque原理
-
deque是一个双向开口的容器,所谓双向开口就是在头尾两端均可以做元素的插入和删除操作。
-
deque相比于vector最大的差异就在于,支持常数时间内对首尾两端进行插入和删除操作,而且deque没有容量的概念,其内部采用分段连续内存空间来存储元素,在插入元素的时候随时都可以重新增加一段新的空间并链接起来。
-
deque提供了Ramdon Access Iterator,同时也支持随机访问和存取,但是它也为此付出了昂贵的代价,其复杂度不能跟vector的原生指针迭代器相提并论。
-
动态开辟的二维数组空间,第二维固定长度的数组空间,扩容的时候(第一维的数组进行2倍扩容)。
deque内部实现的是一个双向队列。元素在内存连续存放。随机存取任何元素都在常数时间完成(仅次于vector)。所有适用于vector的操作都适用于deque。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
-
deque的中控器
deque为了维持整体连续的假象,设计一个中控器,其用来记录deque内部每一段连续空间的地址。大体上可以理解为deque中的每一段连续空间分布在内存的不连续空间上,然后用一个所谓的map作为主控,记录每一段内存空间的入口,从而做到整体连续的假象。
- deque的迭代器是怎么回事呢?
deque提供的是一个随机访问迭代器,由于是分段连续空间,其必须记录当前元素所在段的信息,从而在该段连续空间的边缘进行前进或者后退的时候能知道跳跃到的上一个或下一个缓冲区。deque必须完完整整的掌握和控制这些信息,以达到正确的跳跃。
- deque的数据结构
deque维护着一个map,用来记录每个缓冲区的位置。除了map外,deque的数据结构还维护着start和finish两个迭代器,分别指向deque的首尾。此外,他还必须知道map的大小,一旦map提供的节点不足,就需要配置一块更大的map。
参考
C++面试-STL篇,细节有点多 - cpp后端技术的文章 - 知乎