目录
请你说说 vector 的扩容机制,扩容以后,它的内存地址会变化吗?
说一说 vector 和 list 的区别,分别适用于什么场景?
更多见:C++面试题系列
使用过哪些容器,底层如何实现的?
** 标准回答**
STL 中容器分为顺序容器、关联式容器、无序关联式容器、容器适配器三种类型,三种类型容器特性分别如下:
1. 顺序容器 又叫序列式容器,容器并非排序的,元素的插入位置同元素的值无关,包含 vector、deque、list、array、forward_list 。
- vector:动态数组 元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
- deque:双向队列 元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于 vector )。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
- list:双向链表 元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
2. 关联式容器 元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现,包含set、multiset、map、multimap。
- set/multiset set中不允许相同元素,multiset 中允许存在相同元素。
- map/multimap map 与 set 的不同在于 map 中存放的元素有且仅有两个成员变,一个名为 first,另一个名为 second,map 根据 first 值对元素从小到大排序,并可快速地根据 first 来检索元素。map 和multimap 的不同在于是否允许相同 first 值的元素。
3. 无序关联式容器 unordered_map、unordered_multimap、unordered_set、unordered_multiset
4. 容器适配器 封装了一些基本的容器,使之具备了新的函数功能,包含 stack、queue、priority_queue。
- stack:栈 栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项),后进先出。
- queue:队列 插入只可以在尾部进行,删除、检索和修改只允许从头部进行,先进先出。
- priority_queue:优先级队列 内部维持某种有序,然后确保优先级最高的元素总是位于头部,最高优先级元素总是第一个出列。
STL 容器用过哪些,查找的时间复杂度是多少,为什么?
标准回答
STL 中常用的容器有 vector、deque、list、map、set、multimap、multiset、unordered_map、unordered_set 等。
容器底层实现方式及时间复杂度分别如下:
1. vector 采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N)
2. deque 采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N)
3. list 采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为: 插入: O(1) 查看: O(N) 删除: O(1)
4. map、set、multimap、multiset 上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为: 插入: O(logN) 查看: O(logN) 删除: O(logN)
5. unordered_map、unordered_set、unordered_multimap、 unordered_multiset 上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N) 查看: O(1),最坏情况O(N) 删除: O(1),最坏情况O(N)
注意:容器的时间复杂度取决于其底层实现方式。
简述 vector 的实现原理
得分点 动态数组、连续存储空间、扩容
标准回答
vector 是一种动态数组,在内存中具有连续的存储空间,支持快速随机访问,由于具有连续的存储空间,所以在插入和删除操作方面,效率比较慢。
当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。
vector 容器扩容的过程需要经历以下 3 步:
1. 完全弃用现有的内存空间,重新申请更大的内存空间;
2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
3. 最后将旧的内存空间释放。
vector 扩容是非常耗时的。为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。
请你说说 deque 的实现原理
得分点
分段连续内存、中控器
标准回答
deque 是由一段一段的定量的连续空间构成。
一旦有必要在 deque 前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在 deque 的头端或者尾端。
deque 最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
既然 deque 是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。
deque 采取一块所谓的 map(不是 STL 的 map 容器)作为主控,这里所谓的 map 是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是 deque的存储空间的主体。
请你说说 set 的实现原理
得分点 红黑树、排序
标准回答
set 底层使用红黑树实现,一种高效的平衡检索二叉树。
set 容器中每一个元素就是二叉树的每一个节点,
对于 set 容器的插入删除操作,效率都比较高,原因是二叉树的删除插入元素并不需要进行内存拷贝和内存移动,只是改变了指针的指向。
对 set 进行插入删除操作 都不会引起迭代器的失效,因为迭代器相当于一个指针指向每一个二叉树的节点,对 set的插入删除并不会改变原有内存中节点的改变。
set 中的元素都是唯一的,而且默认情况下会对元素进行升序排列。
不能直接改变元素值,因为那样会打乱原本正确的顺序,要改变元素值必须先删除旧元素,再插入新元素。
不提供直接存取元素的任何操作函数,只能通过迭代器进行间接存取。
请你说说迭代器失效原因,有哪些情况
标准回答
STL 中某些容器调用了某些成员方法后会导致迭代器失效。
例如 vector 容器,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector 容器的元素可能已经被复制或移到了新的内存地址。
1. 序列式容器迭代器失效
对于序列式容器,例如 vector、deque,由于序列式容器是组合式容器,当当前元素的迭代器被删除后,其后的所有元素的迭代器都会失效,这是因为 vector、deque都是连续存储的一段空间,所以当对其进行 erase 操作时,其后的每一个元素都会向前移一个位置。
解决:erase 返回下一个有效的迭代器。
2. 关联式容器迭代器失效
对于关联容器,例如如 map、 set,删除当前的迭代器,仅仅会使当前的迭代器失效,只要在 erase 时,递增当前迭代器即可。这是因为 map 之类的容器,使用了红黑树来实现,插入、删除一个节点不会对其他点造成影响。erase 迭代器只是被删元素的迭代器失效,但是返回值为 void,所以要采用 erase(iter++) 自增方式删除迭代器。
有没有使用过仿函数?
介绍一下STL的allocaotr
面试题(60)|STL(6):map常见面试题和用法总结
STL里resize和reserve的区别
面试题(55)|STL(5):vector删除指定值的元素
请你说说 vector 的扩容机制,扩容以后,它的内存地址会变化吗?
得分点 申请空间、拷贝数据、释放旧空间
标准回答
当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。
vector 容器扩容的过程需要经历以下 3 步:
1. 完全弃用现有的内存空间,重新申请更大的内存空间;
2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
3. 最后将旧的内存空间释放。
因为 vector 扩容需要申请新的空间,所以扩容以后它的内存地址会发生改变。vector 扩容是非常耗时的,为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。
说一说 vector 和 list 的区别,分别适用于什么场景?
得分点 低层数据结构、内存顺序、是否支持随机访问
标准回答
1. 区别
- vector 底层实现是数组,list 是双向链表 - vector 支持随机访问,list 不支持
- vector 是顺序内存,list 不是
- vector 在中间节点进行插入删除会导致内存拷贝,list 不会
- vector 一次性分配好内存,不够时才进行扩容,list 每次插入新节点都会进行内存申请
- vector 随机访问性能好,插入删除性能差,list 随机访问性能差,插入删除性能好
2. 适用场景
- vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用 vector。
- list 拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。