C++方向知识汇总(三)
关于Vector
1.vector是什么?常用场景?底层用了什么数据结构?
答:
vector是动态数组,支持快速随机访问、适合频繁读取、尾部增删的场景(如有序数组遍历)
底层是基于连续分配的数组,类似于动态扩容数组,采用三个指针实现
2.vector的扩容机制是怎样的?扩容时会发生什么?
答:
默认按1.5倍扩容,vs下是2倍
扩容分三步:申请新内存、拷贝原数据到新内存、释放原内存(原迭代器失效)
3.vector的reserve和resize区别?
答:
reserver进行扩容,不改变size个数、提前预留空间
resize改变size个数,可能触发扩容,多余元素初始化0,小于原size则销毁多余元素,容量不变
4.请解释vector容器和他的特点?
答:
vector是c++标准模板库STL里的容器,就是一个动态数组、空间不够自动扩容、支持随机访问、可通过索引直接访问元素、事件复杂度O1,可以高效尾插尾删、中间和开头效率低
5.vector如何保证元素的连续存储
答:
使用动态分配的数组空间存储元素,分配空间在堆上,是连续的,元素不足扩容时,还是使用的连续空间,把旧数据拷贝到新连续空间上。保证了数据的连续存储
6. vector的push_back和emplace_back有什么区别?
答:
push_back需要传入对象,emplace_back可以直接传入对象的参数
push_back会触发拷贝构造,emplace_back直接在容器原地构造
push_back即使会触发移动构造,效率也不及emplace_back
7.vector的增删都是怎么做的?为什么是1.5倍或2倍扩容?
答:
push_back和insert插入 pop_back和erase删除
1.5倍或者2倍是为了防止频繁扩容,也为了减少内存浪费,1.5倍相比2倍内存浪费更少
8.vector扩容后,迭代器会怎样?
答:
所有迭代器失效
9.vector和数组有什么区别?
答:
数组大小固定,编译器确定大小,vector大小自动扩容
数组在栈上开辟空间,或者动态数组堆上,vector在堆上,自动管理内存
10.vector的底层数据结构是什么?
答:
vector的底层数据结构是动态数组
11.如何避免vector频繁扩容?
答:
提前预留空间使用reserve,避免频繁扩容
关于List
1.list的底层结构是什么?有什么特点?
答:
list底层是双向循环链表,每个节点包括数据和两个指针(前驱节点和后驱节点)
特点:内存空间不是连续的,不需要提前开辟固定大小空间
插入删除速度快,时间复杂度O1,但不支持随机访问,需从头遍历,时间复杂度On
删除只会当前迭代器失效,不影响其他迭代器
2.vector和list的核心区别是什么?各自适用场景?
答:
vector 动态数组 || 随机访问O1 || 中间插删效率低On || 可能频繁扩容 || 可能迭代器失效
list 双向链表| | 不支持随机访问On || 中间插删效率高O1 || 不存在频繁扩容 || 仅当前迭代器失效
vector适合需要频繁访问的,以尾插尾删为主的(如存储数据然后频繁查询)
list适合需要中间开头频繁插入删除的,无需随机访问的场景(如链表操作、队列)
3.list有哪些常用成员函数?与vector相比有什么特殊点?
答:
push_back push_front pop_back pop_front insert erase splice sort
特殊:有头插头删 有splice方法 自带sort成员函数,因为不支持随机访问无法使用全局STL算法
,需使用自身的排序,时间复杂度Onlogn
4.list的迭代器为什么是双向迭代器,而不是随机访问迭代器?
答:
vector是连续数组,迭代器可通过+n -n 直接跳转
list是双向链表,节点地址不在一块,不连续,只能通过++ -- 来移动,每次一步,因为要双向迭代器
5.list如何避免内存碎片?与vector相比内存效率如何?
答:
list每个节点单独分配内存、释放也是单独释放、频繁插入删除可能导致小量内存碎片
vector是大块连续内存,扩容可能一次性释放旧内存、内存碎片相对较少
总体而言,list内存利用率低,每个节点还需要存储前驱后驱、但没有vector扩容时的峰值内存消耗
6.如何实现一个线程安全的list?
答:
线程安全需保证多线程对list的操作不需要资源竞争
方案:对List的所有操作加锁,确保某一时刻只有一个在访问
用读写锁,运行多个线程访问,不允许多个修改写入
关于map/set/unordered_map/unordered_set
1.map/set的底层数据结构是什么?为什么选择这种结构?
答:
底层是红黑树,一种近似平衡的二叉搜索树
选择它是因为有序性和高效率操作
红黑树通过严格的规则、使插入删除查找操作都在Ologn时间复杂度
相比AVL树,旋转更少、性能更佳
红黑树中序遍历是有序的,满足map/set的要求
2.map/set、multimap、multiset的核心区别是什么?
答:
map 存储键值对 key唯一 不允许重复
multimap key可以不唯一,可以重复,但是不支持[]了,因为key重复了,无法确定找那个
set 存储键值对,但key=value 不可以重复
multiset 可以重复,可能存储多个相同元素
简单来说:map/set 有去重的功能
3.与哈希容器的对比
答:
map 红黑树 有序 增删查Ologn 内存占用较低(仅存储数据和指针) 删除不影响其他迭代器
unordered_map 哈希表 无需 增删查O1,查找最坏On 内存占用较高(哈希表需预留空间) 扩容所有迭代器失效
哈希追求极致效率、适合不关心有序的场景(如缓存,哈希表计数等)
4.为什么unordered_map的查找效率平均O1,最坏On?
答:
unordered_map底层是哈希表,有时候会产生哈希冲突,大量的哈希冲突,导致几个数据被存在一个长链表/桶中,需遍历链表 时间复杂度就上去了
5.如何解决哈希冲突?
答:
可以优化,采用开链法,冲突时在链表后追加节点、链表超过指定长度转为红黑树
动态扩容,根据负载因子,超过阈值如0.7,自动扩容哈希表,重新哈希映射所有元素
6.map的insert和[]操作有什区别?
答:
两者都可以插入键值对,但是:
insert 当key不存在时插入,若存在不做操作,返回一个pair
[] 当key不存在时插入、新键值对、若存在直接修改对应的value
想要插入但不覆盖用insert,想要插入或者更新用[]
7.如何高效遍历map/set?迭代器有什么特点?
答:
迭代器遍历或者范围for遍历
特点:是双向迭代器、因为支持++ -- 不支持+n -n,跟list一样
删除只导致当前迭代器失效,不影响其他的
8.当自定义类型作为map的key或者set的元素时,需要做什么?
答:
需要自定义比较规则,因为需要排序,根据规则插入
在自定义类型中重载< 或者自定义一个仿函数
关于容器适配器stack、queue、priority_queue、deque
1. 容器适配器 stack、queue、priority_queue 的底层默认容器是什么?能否更换底层容器?
答:
stack 基于deque实现(也可以vector或list)
queue 基于 deque实现(也可以list)
priority_queue 基于vector实现(配合堆算法,也可以使用deque)
2. stack 和 queue 为什么默认选择 deque 作为底层容器,而不是 vector 或 list?
答:
平衡性的结果 stack和queue都需要高效的尾部头部插入删除 deque刚好满足 list缓存利用低 所以选用deque
deque分段连续内存、扩容成本比vector低
list节点不连续、缓存利用率低