STL
STL概况
面试题
STL常用的容器有哪些以及各自的特点是什么?
1.vector:底层数据结构为数组 ,支持快速随机访问。 2.list:底层数据结构为双向链表,支持快速增删。 3.deque:底层数据结构为一个中央控制器和多个缓冲区,支持首尾(中间不能)快速增删,也支持随机访问。 4.stack:底层一般用23实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时 5.queue:底层一般用23实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装) 6.priority_queue:的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现 7.set:底层数据结构为红黑树,有序,不重复。 8.multiset:底层数据结构为红黑树,有序,可重复。 9.map:底层数据结构为红黑树,有序,不重复。 10.multimap:底层数据结构为红黑树,有序,可重复。 11.hash_set:底层数据结构为hash表,无序,不重复。 12.hash_multiset:底层数据结构为hash表,无序,可重复 。 13.hash_map :底层数据结构为hash表,无序,不重复。 14.hash_multimap:底层数据结构为hash表,无序,可重复。
使用场景
1、如果你需要高效的随机存取,而不在乎插入和删除的效率,使用vector
2、如果你需要大量的插入和删除,而不关心随机存取,则应使用list
3、如果你需要随机存取,而且关心两端数据的插入和删除,则应使用deque。
4、如果你要存储一个数据字典,并要求方便地根据key找value,那么map是较好的选择
5、如果你要查找一个元素是否在某集合内存中,则使用set存储这个集合比较好
vector 和 list 的区别
1) vector, 连续存储的容器,动态数组,在堆上分配空间 ;
底层实现:数组。
如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素。
适用场景:经常随机访问,且不经常对非尾节点进行插入删除。
2)list,动态链表,在堆上分配空间,每插入一个元素都会分配空间,每删除一个元素都会释放空间。
底层:双向链表
访问:随机访问性能很差,只能快速访问头尾节点。
适用场景:经常插入删除大量数据
3) vector在中间节点进行插入删除会导致内存拷贝,list不会。
4) vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
5) vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
vector每次insert或erase之后,以前保存的iterator会不会失效?
理论上会失效,理论上每次insert或者erase之后,所有的迭代器就重新计算的,所以都可以看作会失效,原则上是不能使用过期的内存,但是vector一般底层是用数组实现的,我们仔细考虑数组的特性,不难得出另一个结论,insert时,假设insert位置在p,分两种情况:
a) 容器还有空余空间,不重新分配内存,那么p之前的迭代器都有效,p之后的迭 代器都失效
b) 容器重新分配了内存,那么p之后的迭代器都无效,erase时,假设erase位置在p,则p之前的迭代器都有效并且p指向下一个元素位置(如果之前p在尾巴上,则p指向无效尾end),p之后的迭代器都无效
STL对于小内存块请求与释放的处理
STL考虑到小型内存区块的碎片问题,设计了双层级配置器,第一级配置直接使用malloc()和free();第二级配置器则视情况采用不同的策略,当配置区大于128bytes时,直接调用第一级配置器;当配置区块小于128bytes时,便不借助第一级配置器,而使用一个memory pool来实现。究竟是使用第一级配置器还是第二级配置器,由一个宏定义来控制。SGI STL中默认使用第二级配置器。
二级配置器会将任何小额区块的内存需求量上调至8的倍数,(例如需求是30bytes,则自动调整为32bytes),并且在它内部会维护16个free-list, 各自管理大小分别为8, 16, 24,…,128bytes的小额区块,这样当有小额内存配置需求时,直接从对应的free list中拔出对应大小的内存(8的倍数);当客户端归还内存时,将根据归还内存块的大小,将需要归还的内存插入到对应free list的最顶端。
小结:
STL中的内存分配器实际上是基于空闲列表(free list)的分配策略,最主要的特点是通过组织16个空闲列表,对小对象的分配做了优化。
1)小对象的快速分配和释放。当一次性预先分配好一块固定大小的内存池后,对小于128字节的小块内存分配和释放的操作只是一些基本的指针操作,相比于直接调用malloc/free,开销小。
2)避免内存碎片的产生。零乱的内存碎片不仅会浪费内存空间,而且会给OS的内存管理造成压力。
3)尽可能最大化内存的利用率。当内存池尚有的空闲区域不足以分配所需的大小时,分配算法会将其链入到对应的空闲列表中,然后会尝试从空闲列表中寻找是否有合适大小的区域,
但是,这种内存分配器局限于STL容器中使用,并不适合一个通用的内存分配。因为它要求在释放一个内存块时,必须提供这个内存块的大小,以便确定回收到哪个free list中,而STL容器是知道它所需分配的对象大小的,比如上述:
stl::vector array;
array是知道它需要分配的对象大小为sizeof(int)。一个通用的内存分配器是不需要知道待释放内存的大小的,类似于free(p)。
map 和 set 有什么区别
1) map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。
2) map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。
3) set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。
4) map支持下标操作,set不支持下标操作。map可以用key做下标。
unordered_map和map
内部实现机理
- map: map内部实现了一个红黑树,该结构具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素,因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行这样的操作,故红黑树的效率决定了map的效率。
- unordered_map: unordered_map内部实现了一个哈希表,因此其元素的排列顺序是杂乱的,无序的
优缺点以及适用处
map优点:
- 有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
- 红黑树,内部实现一个红黑书使得map的很多操作在的时间复杂度下就可以实现,因此效率非常的高
- 对于那些有顺序要求的问题,用map会更高效一些
map 缺点: 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点,孩子节点以及红/黑性质,使得每一个节点都占用大量的空间
unordered_map 优点:
因为内部实现了哈希表,因此其查找速度非常的快
unordered_map 缺点:
对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map,哈希表的建立比较耗费时间
STL 中迭代器的作用,有指针为何还要迭代器
1) Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
2) 迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、--等,相当于一种智能指针。
3) 迭代器产生原因:Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
STL 迭代器是怎么删除元素的呢
1) 对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;
2) 对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
3) 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator。
平衡二叉树(AVL树)和红黑树
1)平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。
2)红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑),红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。
3)所以红黑树在查找,插入删除的性能都是O(logn),且性能稳定,所以STL里面很多结构包括map底层实现都是使用的红黑树。
哈希表(hash表)
哈希表的实现主要包括构造哈希和处理哈希冲突:构造哈希,主要包括直接地址法,除留余数法。
处理哈希冲突:当哈希表关键字集合很大时,关键字值不同的元素可能会映射到哈希表的同一地址上,这样的现象称为哈希冲突。常用的解决方法有:
1) 开放定址法,冲突时,用某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。(如,线性探测,平方探测)
2) 再哈希法:当发生冲突时,用另一个哈希函数计算地址值,直到冲突不再发生。
3) 链地址法:将所有哈希值相同的key通过链表存储,key按顺序插入链表中。
参考