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是一个双向开口的容器,所谓双向开口就是在头尾两端均可以做元素的插入和删除操作。

  • 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。


5. 优先级队列

底层数据结构以vector为底层容器,heap为处理规则管理底层容器。

优先队列(priority_queue)容器与队列一样,只能从队尾插入元素,从队首删除元素。但是它有一个特性,队列中最大的元素总是位于队首。出队时,并非按照先进先出的原则进行,而是将当前队列中最大的元素出队。


6. map和unordered_map嘛?底层实现呢

  • map实现机制

map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。

对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。

  • unordered_map实现机制

unordered_map内部实现了一个哈希表(也叫散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。


7. set的底层实现为什么不用哈希表而是用红黑树

  • set中元素是经过排序的,红黑树也是有序的,哈希是无序的

  • 如果只是单纯的查找元素的话,那么肯定要选哈希表了,因为哈希表在的最好查找时间复杂度为O(1),并且如果用到set中那么查找时间复杂度的一直是O(1),因为set中是不允许有元素重复的。而红黑树的查找时间复杂度为O(logn)。


8. map和set和插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效

因为存储的是节点,不需要内存拷贝和内存移动。

插入操作只是节点指针换来换去,节点内存没有改变,而iterator就像指向节点的指针,内存没变,指向内存de指针也不会变。

map和set底层实现都是红黑树;map和set的区别在于map的值不作为键,键和值是分开的。


9. map中[ ]与find的区别

  • map的下标运算符[ ]的作用是:将关键码作为下标去执行查找,并返回对应的值;如果不存在这个关键码,就将一个具有该关键码和值类型的默认值的项插入这个map。

  • map的find函数:用关键码执行查找,找到了返回该位置的迭代器;如果不存在这个关键码,就返回尾迭代器。


10. STL 中有哪些常见的容器

顺序容器、关联式容器、容器适配器三种类型。

  • 顺序容器 容器并非排序的,元素的插入位置同元素的值无关,包含 vector、deque、list。

  • 关联式容器元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现,包含set、map。

  • 容器适配器 封装了一些基本的容器,使之具备了新的函数功能,包含 stack、queue。


11. 迭代器失效原因有哪些?

  • 插入和删除操作: 当在容器中插入或删除元素时,可能会导致容器内存重新分配或元素位置的改变,这可能会使迭代器失效。

  • 清空容器: 清空容器会使容器内的所有元素被删除,这样迭代器指向的元素就会失效。

  • 使用引起重新分配的操作: 例如,在vector中使用push_back()添加元素时,如果超出了当前容量,可能会触发重新分配操作,从而使所有迭代器失效。

  • 排序操作: 如果在排序过程中,容器的元素被移动了位置,迭代器可能会失效。



参考

C++面试-STL篇,细节有点多 - cpp后端技术的文章 - 知乎

C/C++工程师面试题(STL篇) - linux的文章 - 知乎

C/C++工程师面试题(STL篇) - linux的文章 - 知乎

相关推荐
NoneCoder4 分钟前
Python入门(12)--数据处理
开发语言·python
六月的翅膀11 分钟前
C++:实例访问静态成员函数和类访问静态成员函数有什么区别
开发语言·c++
Domain-zhuo17 分钟前
什么是JavaScript原型链?
开发语言·前端·javascript·jvm·ecmascript·原型模式
SoraLuna18 分钟前
「Mac玩转仓颉内测版24」基础篇4 - 浮点类型详解
开发语言·算法·macos·cangjie
小丁爱养花24 分钟前
前端三剑客(三):JavaScript
开发语言·前端·javascript
生信摆渡39 分钟前
R语言-快速对多个变量取交集
开发语言·数据库·r语言
liujjjiyun41 分钟前
小R的随机播放顺序
数据结构·c++·算法
¥ 多多¥1 小时前
c++中mystring运算符重载
开发语言·c++·算法
Mr.Pascal1 小时前
刚学php序列化/反序列化遇到的坑(攻防世界:Web_php_unserialize)
开发语言·安全·web安全·php
小尤笔记1 小时前
利用Python编写简单登录系统
开发语言·python·数据分析·python基础