STL标准模板库

一、包含了什么

容器---容纳一组元素的对象

迭代器---提供一种访问容器中每个元素的方法

仿函数---一个行为类似函数的对象,调用它就像调用函数一样

适配器---用来修饰容器 比如queue stack 底层借助了deque

空间配置器---负责空间配置和管理                  算法

二、容器

顺序容器:
vector:向量容器。底层是动态开辟的一维数组,内存可自增长,每次增长 2 倍
deque:双端队列容器。底层是动态开辟的二维数组
list:列表容器。底层是带头结点的双向链表容器
关联容器
set:单重集合。底层实现是红黑树。不允许重复元素。
multiset:多重集合。底层实现是红黑树。允许重复元素。
map:单重映射表。底层实现是红黑树
multimap:多重映射表。底层实现是红黑树
容器适配器
stack:栈。底层默认依赖 deque 容器来实现一个先进后出,后进先出的栈结构
queue:队列。底层默认依赖 deque 实现一个先进先出,后进后出的队列结构
priority_queue:优先级队列。底层默认依赖 vector 实现一个大根堆结构,默认值越大,优先级越高

1、vector

vector 底层是一个内存可 2 倍增长的一维数组,因此 vector 适合随机访问和末尾的增加删除场景,它的时间复杂度是 O(1)。

vector就是一个动态数组,里面有一个指针指向一片连续的内存空间,当空间不够装下数据时,会自动申请另一片更大的空间,然后把原来的数据拷贝过去,接着释放原来的那片空间;当释放或者删除里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。

(1)vector的底层原理

 vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。

  当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间【vector内存增长机制】

  当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。

  因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了。

(2)vector中的reserve和resize的区别

  reserve是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),就可以提高效率,其次还可以减少多次要拷贝数据的问题。reserve只是保证vector中的空间大小(capacity)最少达到参数所指定的大小n。reserve()只有一个参数。

  resize()可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。resize()可以有多个参数。

(3)vector的元素类型可以是引用吗?

  vector的底层实现要求连续的对象排列,引用并非对象,没有实际地址,因此vector的元素类型不能是引用。

(4)vector迭代器失效的情况

  当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。

  当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);

(5)vector 扩容为什么要以1.5倍或者2倍扩容?

  根据查阅的资料显示,考虑可能产生的堆空间浪费,成倍增长倍数不能太大,使用较为广泛的扩容方式有两种,以2倍的方式扩容,或者以1.5倍的方式扩容。以2倍的方式扩容,导致下一次申请的内存必然大于之前分配内存的总和,导致之前分配的内存不能再被使用,所以最好倍增长因子设置为(1,2)之间.

2、deque

双端队列,底层是一个动态开辟的二维数组(称作 deque_map,但它不是映射表 map,就是个动态开辟的二维数组而已,一维的数组
放的全是指针,指向二维的分段连续的缓冲区 buffer)

deque是双向开口(在头尾分别做元素删除和插入操作)的连续线性空间,适合随机访问和在头尾两端进行元素的插入跟删除操作。

deque和vector的最大差异:

(1)deque允许在头部快速进行元素插入或删除操作,头尾插时间复杂度都是O(1);

(2)deque没有容量(capacity)的概念,因为它是动态地以分段连续空间组合而成的,随时可以增加一段新空间并链接起来。不会像vector那样“因旧空间不够而新配置一个更大空间,然后复制元素,再释放旧空间”。

(3) vector 适合末尾的插入删除(单向开口),也可头插(时复O(n)),deque适合在头尾两端进行元素的插入删除(双向开口)

(4)从中间进行插入,删除等操作,由于 vector 底层内存是绝对连续的,因此其效率要比deque高,所以我们一般在使用队列的场景下,首尾增删比较多的情况下选择deque,否则一般都选择使用 vector;

但是还有一种场景就是数据量不大的情况下,由于 deque 的第二维数组是事先分配好的内存,可以直接使用,初始操作效率高;而 vector 默认构造底层空间是 0,待添加数据的时候,底层内存才从 1 开始以 2 倍的速度开始扩容,因此 vector 的初始操作效率比较低,好的是 vector 提供了一个 reserve 方法,可以给 vector 容器预留足够的空间。

除非必要,我们应该尽可能使用vector而非deque。对deque进行排序操行,为了最高效率,可将deque先复制到vector中,将vector排序后(用STL sort算法),再复制到deque

 deque是连续空间(至少从逻辑上这样的),连续线性空间让我们想到array或vector。array无法增长,vector虽可以增长,而增长是个假象:(1)要另寻找空间;(2)将原数据复制过去;(3)释放原空间。如果vector每次配置空间没有留下富裕的空间,其增长的代价会很大。

        deque由一段一段的定量连续空间构成。一旦有必要在deque的前端或尾端增加新空间,便配置一段连续空间,串接在整个deque的头或尾部。deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口。这避免了“重新配置、复制、释放”的代价,但是代价是复制的迭代器结构。

        deque要分段连续,那么就要中央控制,而为了维护整体连续的假象,数据结构的设计以及迭代器前进后退等操作都很复杂。deque的代码量远远多于vector和list。

       deque采用一块所谓的map(注意,不是STL的map容器)作为主控。这里所谓map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的储存空间主体。SGI STL 允许我们指定缓冲区大小,默认值0表示将使用512 bytes 缓冲区。


 

3、list

list 底层是一个带头结点的双向链表,只支持双向顺序访问,不支持随机访问,所以它有自己的sort()(库函数的sort支持可随机访问的容器)list 适合任意位置增加和删除情况比较多的场景,它的时间复杂度是 O(1)。

Q:什么情况下用vector,什么情况下用list,什么情况下用deque

  vector可以随机访问元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。

  list不支持随机访问,但是任意位置插入删除效率高,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。

  需要从首尾两端进行插入或删除操作的时候需要选择deque。

4、set   multiset

set单重集合,multiset多重集合,底层实现是红黑树。set中数元素的值不能直接被改变

5、map   multimap

map 是 C++STL 中的一种关联容器,也就是存 key-value 键值对的映射表容器,有 map和 multimap,它们之间的区别是 map 不允许 key 重复,multimap 是允许 key 重复的;map的底层是一颗红黑树,一种非常严格的平衡二叉树,左右子树的高度差不能超过较短子树高度的 2 倍,数据的增删查效率都比较高,平均时间复杂度在 O(log2n)。用它来存储有序的数据。

红黑树的定义:
1)每个结点要么是红的,要么是黑的。
2)根结点必须是黑的。
3)每个叶结点,即空结点(NIL)是黑的。
4)如果一个结点是红的,那么它的俩个儿子都是黑的(父子不能同为红色)
5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。


红黑树不像 AVL(平衡二叉树)树那样维持了二叉树的高度平衡(左右子树的高度差不能超过 1),因此在插入删除数据时,所做的旋转操作比起红黑树来说,那就少很多了,因此其效率也比 AVL 树高;红黑树插入一个新节点,旋转的次数最多 2 次,删除一个节点旋转的
次数最多 3 次。

 二叉查找树(BST)实际上是数据域有序的二叉树,即对树上的每个结点,都满足其左子树上所有结点的数据域均小于或等于根结点的数据域,右子树上所有结点的数据域均大于根结点的数据域。缺陷:如果节点都插入到一边,导致一边的“腿”很长,查找就是线性查找了,效率降低。

   平衡二叉树(AVL)本质还是一棵二叉查找树,只是在其基础上增加了“平衡”的要求。所谓平衡是指,对AVL树的任意结点来说,其左子树与右子树的高度之差的绝对值不超过1,其中左子树与右子树的高度因子之差称为平衡因子。

  • map 、set、multiset、multimap的底层原理

底层实现都是红黑树,epoll模型的底层数据结构也是红黑树,linux系统中CFS进程调度算法,也用到红黑树。

注:在 C++STL 中,map 和 multimap,set 和 multiset 这四种关联容器的底层都是由红黑树来实现的,因此如果要把自定义类类型作为 set 和 map 的元素类型的话,一定要给自定义类型提供operator>或者operator<比较运算符的重载函数,因为红黑树是一棵二叉排序树,
入 set 和 map 的元素都是要经过排序的

Q1:为何map和set的插入删除效率比用其他序列容器高?

不需要做内存拷贝和内存移动。map和set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点就OK了。这里的一切操作就是指针换来换去,和内存移动没有关系

Q2:为何每次insert之后,以前保存的iterator不会失效?

iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。

Q3:当数据元素增多时(10000和20000个比较),map和set的插入和搜索速度变化如何?

如果你知道log2的关系你应该就彻底了解这个答案。在set中查找是使用二分查找,也就是说,如果有16个元素,最多需要比较4次就能找到结果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已

内存分配算法:
STL的优势并不在于算法,而在于内存碎片。如果你需要经常自己去new一些节点,当节点特别多,而且进行频繁的删除和插入的时候,内存碎片就会存在,而STL采用自己的Allocator分配内存,以内存池的方式来管理这些内存,会大大减少内存碎片,从而会提升系统的整体性能。当时间运行很长时间后(例如后台服务程序),map的优势就会体现出来。从另外一个方面讲,使用map会大大降低你的编码难度,同时增加程序的可读性。

6、stack  底层默认依赖 deque 容器来实现一个先进后出,后进先出的栈结构

7、queue  底层默认依赖 deque 实现一个先进先出,后进后出的队列结构

8、priority_queue 优先级队列。底层默认依赖 vector 实现一个大根堆结构,默认值越大,
优先级越高

因为优先级队列底层默认是一个大根堆,因此在一个内存连续的容器 vector(底层是一维数组)上来构建效率高。

9、unordered_map 与map的区别?使用场景?

unordered_map(无序)的底层是一个防冗余的哈希表(采用除留余数法)。哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,时间复杂度为O(1);而代价仅仅是消耗比较多的内存。

  使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数(一般使用除留取余法),也叫做散列函数),使得每个元素的key都与一个函数值(即数组下标,hash值)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照key为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方,称为桶。

构造函数:unordered_map 需要hash函数,等于函数;map只需要比较函数(小于函数).

存储结构:unordered_map 采用hash表存储,map一般采用红黑树(RB Tree) 实现。因此其memory数据结构是不一样的。

  总体来说,unordered_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n)小,hash还有hash函数的耗时,如果你考虑效率,特别是在元素达到一定数量级时,考虑考unordered_map 。但若你对内存使用特别严格,希望程序尽可能少消耗内存,那么一定要小心,unordered_map 可能会让你陷入尴尬,特别是当你的unordered_map 对象特别多时,你就更无法控制了,而且unordered_map 的构造速度较慢。

三、迭代器失效

(1)插入操作

对于vector和string,如果容器内存被重新分配,iterator失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效;

对于deque,如果插入点位于除front和back的其它位置,iterator失效;当我们插入元素到front和back时,deque的迭代器失效,(都失效)

对于list和forward_list,所有的iterator有效。

(2)删除操作

对于vector和string,删除点之前的有效,删除点之后失效

对于deque,如果删除点位于除front和back的其它位置,iterators,失效;当我们插入元素到front和back时, iterators有效;

对于list和forward_list,所有的iterator有效。

对于关联容器(如map, set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响。erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。 
erase(it++)的执行过程:这句话分三步走,先把iter传值到erase里面,然后iter自增,然后执行erase,所以iter在失效前已经自增了。 
map是关联容器,以红黑树或者平衡二叉树组织数据,虽然删除了一个元素,整棵树也会调整,以符合红黑树或者二叉树的规范,但是单个节点在内存中的地址没有变化,变化的是各节点之间的指向关系。

四、STL里sort算法用的是什么排序算法?

快速排序,还结合了插入排序和堆排序。

哪些STL容器需要用到sort算法?

首先,关系型容器拥有自动排序功能,因为底层采用RB-Tree,所以不需要用到sort算法。

其次,序列式容器中的stack、queue和priority-queue都有特定的出入口,不允许用户对元素排序。

剩下的vector、deque,适用sort算法。

STL的sort算法,数据量大时采用快排算法,分段归并排序。一旦分段后的数据量小于某个门槛(16),为避免快排的递归调用带来过大的额外负荷,就改用插入排序。如果递归层次过深,还会改用堆排序。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值