关注本人公众号,获取更多学习资料!
微信公众号搜索:阿Q正砖
上期说过C++这块面试问的东西也蛮多,简历上只要出现C++这几个字,那么STL库就是必问。
总不能是面试官问你了解STL库吗?你尴尬的说这块不怎么熟悉。那这就…
总的来说STL库这块也不是那么难理解,算了…说太多容易挨打,直接就进入正题吧。
STL库
一、基本概念
1、分类
标准序列容器:vector、string、deque和list。
标准关联容器:set、multiset、map和multimap。
非标准序列容器:slist和rope。slist是一个单向链表,rope本质上是一个“重型”string。
非标准关联容器:hash_set、hash_muliset、hash_map、hash_multimap。
2、六大组件
容器(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。
3、迭代器
输入迭代器:是只读迭代器,在每个被遍历到的位置上只能被读取一次。
输出迭代器:是只写迭代器,在每个被遍历到的位置上只能被写入一次。
前向迭代器:兼具输入和输出迭代器的能力,但是它可以对同一个位置重复进行读和写。前向迭代器不支持operator–,所以它只能向前移动。所有的标准STL容器都支持比前向迭代器功能更强大的迭代器。
双向迭代器:很像前向迭代器,只是它们向后移动和向前移动同样容易。标准关联容器都提供了双向迭代器。list也是如此。
随机访问迭代器:有双向迭代器的所有功能,而且,它还提供了“迭代器算术”,即在一步内向前或向后跳跃的能力。vector、string和deque都提供了随机访问迭代器。指向数组内部的指针对于数组来说也是随机访问迭代器。
二、底层原理及相关面试题
1、Vector
vector底层是一个动态数组,内存是连续的,每次以原来空间大小的2倍来进行扩容的。
(1)容器中,对象的构造析构,内存的开辟释放,通过容器的空间配置器allocator来实现的。
(2)增删查
vector vec;
增加:
vec.push_back(); 末尾添加元素O(1),导致容器扩容。
vec.insert(it,20); 迭代器指向的位置添加一个元素20 O(n) 导致容器扩容。
删除:
vec.pop_back(); 末尾删除元素O(1)。
vec.erase(it); 删除it迭代器指向的元素O(n)。
查询:
operator[] 下标的随机访问vec[5] O(1)。
iterator迭代器进行遍历。
注意:对容器进行连续插入或者删除操作(insert/erase),一定要更新迭代器,否则第一次insert或者erase完成,迭代器就失效了。
(3)常用的函数
reserve(20):vector预留空间的,只给容器底层开辟指定大小的内存空间并不会添加新的元素。
resize(20):容器扩容用的,不仅给容器底层开辟指定大小的内存空间,还会添加新的元素。
swap:两个容器进行元素交换。
(4)reserve()和resize()的区别
reserve()是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题,这样的话就可以提高效率,它还可以减少多次要拷贝数据的问题。reserve()只有一个参数。
resize()可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。resize()可以有多个参数。
(5)size()和capacity()的区别
size表示当前vector中有多少个元素。
capacity表示它已经分配的内存中可以容纳多少元素。
(6)迭代器失效情况
当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。
当删除容器中一个元素后,待迭代器所指向的元素已经被删除,也会造成迭代器失效。erase()方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);。
2、Deque
deque是一个双向开口的容器,所谓双向开口就是在头尾两端均可以做元素的插入和删除操作。(双端队列)
动态开辟的二维数组,第二维固定长度的数组空间,扩容的时候(第一维的数组进行2倍扩容)
(1)deque相比于vector最大的差异就在于支持常数时间内对首尾两端进行插入和删除操作,而且deque没有容量的概念,其内部采用分段连续内存空间来存储元素,在插入元素的时候随时都可以重新增加一段新的空间连接起来。
(2)deque提供了Ramdon Access Iterator,同时也支持随机访问和存取,但是它也为此付出了昂贵的代价,其复杂度不能跟vector的原生迭代器相提并论。
(3)deque的中控器
deque为了维持整体连续的假象,设计一个中控器,其用来记录deque内部每一段连续空间的地址。大体上可以理解为deque中的每一段连续空间分布在内存的不连续空间上,然后用一个所谓的map作为主控,记录每一段内存空间的入口,从而做到整体连续的假象。
(4)deque的迭代器
deque提供的是一个随机访问迭代器,由于是分段连续空间,其必须记录当前元素所在段的信息,从而在该段连续空间的边缘进行前进或者后退的时候能知道跳跃到的上一个或下一个缓冲区。deque必须完完整整的掌握和控制这些信息,以达到正确的跳跃。
buffer_size函数:
static size_t buffer_size(){
return __deque_buf_size(BufSiz, sizeof(T));
}
//如果n不为0,传回n,表示buffer size 由自己定义
如果n为0,表示buffer_size 采用默认值
如果sz(元素大小) < 512,传回512/sz,如果不小于512 ,传回1
inline size_t __deque_buf_size(size_t n, size_t sz)
{
return n != 0 ? n : (sz < 512 ? size_t(512 / sz) : size_t(1));
}
set_node函数:当迭代器处在当前缓冲区的边缘时,一旦前进或者后退,就要考虑超过当前缓冲区的情况,此时需要跳转到下一个缓冲区,这时候set_node就派上用场了。
void set_node(map_pointer new_node)
{
node = new_node; // 跳转到相应缓冲区
first = *new_node; // 更新跳转后缓冲区first信息
last = first + difference_type(buffer_size()); // 更新跳转后缓冲区last的信息
}
(5)deque的数据结构
deque维护着一个map,用来记录每个缓冲区的位置。除了map外,deque的数据结构还维护着start和finish两个迭代器,分别指向deque的首尾。此外,他还必须知道map的大小,一旦map提供的节点不足,就需要配置一块更大的map。
3、List
list的底层是一个双向循环链表,以节点为单位存放数据,节点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。list不支持随机存取,适合需要大量的插入和删除,而不关心随机存取的应用场景。
(1)迭代器
因为list的底层结构为带头结点的双向循环链表,可将迭代器暂且理解为指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
(2)增删改查
增:
使用函数push_front(); 头插
使用函数push_back(); 尾插
使用函数insert(); 在position位置中插入值为val的元素
删:
使用函数pop_front(); 头删
使用函数pop_back(); 尾删
使用函数clear清空list中的有效函数
改:
利用迭代器对list元素进行修改
使用swap交换两个元素
查:
使用find函数查找,这是算法模块实现,不是list的成员接口
(3)list的优缺点
优点:
list的头部、中间插入不需要挪动数据,效率较高,均为O(1)。
list插入数据是新增节点,不需要扩容,因此节省了空间。
缺点:
不支持随机访问,[]操作符和list.at();
底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低。
(4)常用函数
list.push_back(); //在尾部插入一个数据
list.pop_back(); //删除尾部数据
list.push_front(); //在头部插入一个数据
list.size(); //返回容器中实际数据的个数
list.sort(); //排序,默认由小到大
list.erase(); //删除一个元素,参数是迭代器,返回的是删除迭代器的下一个位置
4、Map || Multimap || Set || Multiset
底层原理都是红黑树。
Map 类似于数据库中1:1关系,是一种关联容器,提供一对一的数据处理能力,这种特性使得map类似于数据结构中红黑树。元素默认按键的升序排序。如果迭代器所指向的元素被删除,则该迭代器失效。其它任何增减、删除元素的操作都不会使迭代器失效。所有元素都会根据元素的键值自动被排序。map的所有元素都是pair,同时拥有实值value和键值key。pair的第一个元素被视为键值,第二个元素被视为实值。map不允许俩各元素拥有相同的键值,由于红黑树是一种平衡二叉搜索树,自动排序的效果很不错,所以标准库STLmap都是以红黑树为层级制。又由于map所开放的各种操作接口,红黑树也都提供了,所以几乎所有map的操作行为,都是红黑树的操作行为。
Multimap类似于数据库中1:N关系,是一种关联容器,提供一对多的数据处理能力。
Set类似于数学里的集合,但是set的集合不包含重复的元素。按照键进行排序存储,值必须可以进行比较,可以理解为set就是键和值相等的map。如果迭代器所指向的元素被删除,则该迭代器失效。其它任何增加、删除元素的操作都不会使迭代器失效。
Multiset类似于数学里的集合,multiset的集合包含重复的元素。
5、unordered_map || unordered_set || unordered_multimap || unordered_multiset
unordered_map和unordered_multimap是无序排列的,其底层实现为hash table。hash table最大的优点就是把数据的存储和查找消耗的时间大大降低,时间复杂度为O(1),代价仅仅是消耗比较多的内存。
unordered_set和unordered_multiset底层实现是hash table。
总结:
强烈建议收藏,面试一定会用得到!!!