C++中有哪些容器,以及具体的底层实现?
C++ 常见 STL 容器分为四类。
顺序容器中,vector 是动态连续数组,deque 通常是分段数组加块指针表,list 是双向链表
有序关联容器包括 set、map 及其 multi 版本,通常采用红黑树实现;对应的 unordered 容器采用哈希表
容器适配器中:stack 和 queue 默认基于 deque,priority_queue 默认基于 vector
具体到底层:
vector 通常用三个指针管理一块连续存储空间,分别表示起点、元素末尾和容量末尾,空间不足时重新分配并搬移元素
deque 用块指针表管理多个连续缓冲区,通过块号和块内偏移实现随机访问
list 是双向链表,通常带哨兵节点
map、set 通常常用红黑树,节点保存父子指针、颜色和数据,插入删除后通过旋转和重新着色维持平衡
unordered 系列采用哈希表,通过哈希值定位桶,再进行键比较,元素增多时可能重哈希
stack、queue 通过封装底层容器实现,默认使用 deque
priority_queue 默认使用 vector 存储二叉堆,通过上浮和下沉维护堆性质
你是如何理解链表的?
链表通过指针把节点连接成一个逻辑上的线性序列,节点不要求连续存储。它不支持高效的随机访问,查找通常是 O(n);但是已知操作位置时,增删只需要修改链接,不必搬移其它元素。代价是额外的指针开销和较差的缓存局部性。
你是如何理解 vector 的?
vector 是自动管理内存的动态连续数组,支持常数时间随机访问,缓存局部性好。它通过预留容量减少扩容次数,尾插均摊为常数时间;容量不足时,需要重新分配并搬移元素,中间增删通常是线性时间。使用时要区分 size 和 capacity,并注意扩容导致的迭代器失效。
你是如何理解栈的?
栈是一种后进先出的线性数据结构,只允许在栈顶插入和删除,核心操作是入栈、出栈和访问栈顶。它可以用数组或链表实现;C++ 的 std::stack 默认基于 deque,常用于括号匹配、表达式求值和深度优先搜索。
你是如何理解队列的?
队列是一种先进先出的线性数据结构,在队尾插入、队头删除。通常可以用循环数组或维护头尾指针的链表实现。C++ 的 std::queue 默认基于 deque,常用于 BFS(广度优先搜索)、消息缓冲和任务排队。
你是如何理解 map 的?
map 是键唯一、按键有序的关联容器,通常基于红黑树实现,按键查找、普通插入和删除为对数复杂度。它适合有序遍历和范围查询。使用时要注意键不能直接修改,而且 operator 在键不存在时会插入元素。
你是如何理解 set 的?
set 是元素唯一且有序的关联容器,元素本身就是键,通常基于红黑树实现,查找、普通插入和按键删除为对数复杂度。它根据比较器判断排序和键的等价性,不能通过迭代器直接修改元素,常用于有序去重和范围查询。
vector 和 list 的区别,什么时候使用?
Vector 是连续存储的动态数组,支持 O(1) 随机访问,缓存局部性好,尾部插入均摊 O(1),但中间插入删除通常需要移动元素,扩容是会使所有迭代器、指针和引用失效。
List 是双向链表,不支持随机访问,内存开销大,但已知迭代器位置时,单个元素的插入删除是 O(1),且不会影响其它元素的迭代器和引用。
一般优先使用 vector,适合随机访问、遍历和尾部追加;只有明确需要稳定的元素地址、再已知位置频繁插入删除,或者需要节点拼接时,再考虑 List。
vector 和 array数组 有什么区别?
vector 和 array 都是连续存储,支持 O(1) 随机访问。
vector 大小可以动态变化,元素通常存放在独立的动态存储中,扩容需要迁移元素并使原有迭代器、指针和引用失效。
array 的大小在编译期固定,元素直接包含在容器对象内部,不需要单独分配,也不会扩容。
固定长度用 array,运行时长度或需要增删元素时用 vector。
vector 可以替换成 list 吗?那有其他数据结构可以替换 vector 的吗?
vector 可以在部分场景下换成 list,但如果代码依赖下标访问、连续内存或 std::sort,就不能直接替换。
list 适合已知位置的频繁插入删除,以及要求元素地址稳定的场景。
其它候选包括:需要随机访问和两端插入删除时用 deque;大小在编译期固定时 array。
选择时主要看访问方式、插入删除位置、连续内存要求和迭代器稳定性。
同样大小的 vector 和 list 遍历一遍,谁更快?
相同元素类型、相同元素数量下,遍历一遍通常是 vector 更快。虽然两者的时间复杂度都是 O(N),但是 vector 连续存储,CPU 缓存命中率通常更高,也更有利于 CPU 预取和编译器向量化;list 节点分散,需要追踪指针,且额外指针增加了内存和缓存开销,所以实际遍历通常更慢。
数据结构里面的 vector 和 list 的使用场景
vector 适合随机访问、遍历排序,以及主要在尾部插入删除的场景,通常作为默认选择。
list 适合已经持有迭代、需要频繁插入删除,或者需要稳定元素地址和节点拼接的场景。
如果主要在两端插入删除,也可以考虑 deque。
大量数据增删的情况下用数组还是链表?
通常优先选择动态数组 vector。虽然数组在中间增删需要移动元素,时间复杂度为O(n),但它内存连续,缓存命中率高、额外内存开销小,且能减少频繁分配和释放内存的成本,实际性能往往更好。链表只有在已知操作节点、频繁在中间增删时,才能充分发挥 O(1) 插入和删除的优势。
vector和list底层数据结构有什么不同
Vector 底层是动态连续数组,支持常数时间随机访问,但扩容和中间增删可能搬移元素。
List 底层是双向链表,节点不连续,不支持随机访问,但在已知迭代器位置时,单元素增删时常数时间。
vector 底层原理、vector 扩容机制?如何避免扩容?
vector 底层原理:vector 底层是连续的动态存储,常见实现通过三个指针管理存储起点、元素末尾和容量末尾,并通过分配器将内存分配与对象构造分开。
由于元素连续存储,它支持 O(1) 随机访问。尾部插入时,容量足够久直接构造新元素;容量不足则申请更大的存储,将旧元素移动或拷贝过去,再销毁旧元素并释放旧存储。常见实现采用几何增长,使尾部插入的均摊复杂度为 O(1)。中间插入删除需要移动后续元素,通常为O(n);扩容会导致所以原元素的迭代器、指针和引用失效。
vector 扩容机制:vector 的大小由 size 表示,容量由 capacity 表示。插入元素超过容量时,它会申请更大的连续内存,将已有元素1移动或拷贝过去,再销毁旧元素并释放旧内存。常见实现按约 1.5 倍或 2 倍增长,但没有规定固定倍数。
避免扩容:最常用的方法是提前调用 reserve 预留足够容量;如果元素数量已经确定,也可以直接指定大小或使用 resize,再按下标填充。只有保证后续元素数量不超过容量,才能避免因容量不足而扩容。
emplace_back 和 push_back 区别?
push_back 接收已有对象,通过拷贝或移动构造将其放到容器尾部;emplace_back 通过完美转发接收构造参数,直接在容器尾部构造对象,可以避免中间临时对象及其拷贝或移动。
resize 和 reverse 的区别
resize 调整实际元素数量,增大时构造新元素,缩小时销毁多余元素;
reserve 只预留容量,不改变元素数量,也不构造新元素。
说说 STL 怎么删除元素的?
STL 容器通常通过 erase 删除指定元素,通过 pop_back/pop_front 删除端点元素,通过 clear 清空容器。底层行为取决于容器:vector 删除后需要移动后续元素,list 通过解除节点连接删除,关联容器则维护树或哈希结构。删除时需要注意迭代器失效。
迭代器失效规则?
迭代器失效规则取决于容器。
vector 插入扩容时,迭代器全部失效,不扩容时,插入位置及之后失效,删除也是删除位置及之后失效。
链表和有序关联容器插入不影响已有迭代器,删除只影响被删除元素。
无序关联容器重哈希时全部迭代器失效,否则插入不影响已有迭代器。
vector 中有大量元素,如何删除不需要的元素?
大量删除时,避免循环使用 erase,否则可能退化为平方复杂度
通常使用 remove_if 配合 erase,集中压缩保留元素,再统一删除尾部,整体为线性复杂度
如果不要求保持顺序,可以与尾元素交换后删除,避免移动全部后续元素
谈谈 remove_if?
remove_if 是按谓词逻辑移除元素的算法,谓词返回 true 的元素会被移除。它通过移动保留元素完成压缩,保持相对顺序,返回新的逻辑末尾,但不改变容器大小,所以通常配合 erase 使用,整体时间复杂度为 O(n)。
vector 容量大于 size 时,如何减小容量到 size?
可以使用 shrink_of_fit() 请求缩减容量,但它不保证执行。也可以使用临时 vector 拷贝后与原容器交换,释放原来多余的空间。通常可以缩到 size(),但标准不保证容量一定恰好等于 size()。
vector 的下标 访问和 at() 接口有什么区别 ?为什么 at 抛出异常 , 不抛出,底层如何实现的?
Vector 的 和 at() 都通过连续存储中的首地址加偏移量访问元素,返回引用,复杂度都是 O(1)。
区别是 at() 会检查下标是否小于 size(),越界抛出 std::out_of_range; 要求调用者保证下标合法,常规实现直接解引用,不提供越界异常保证。
它们分别满足直接访问和受检查访问的需求, 越界不能理解为只是"不抛异常",而是违反接口要求。
std::sort 可以对 list 排序吗?
不可以,因为 std::sort 要求随机访问迭代器,而 list 只有双向迭代器。应使用 list.sort(),它通常采用归并排序,复杂度为 O(n logn),并且是稳定排序。
unordered_map 和 vector 的查找复杂度分别是什么?
unordered_map 按键查找平均是 O(1),哈希冲突严重时最坏为 O(N)。
vector 根据下标访问是 O(1),但是按值顺序查找是 O(n)。若元素已经有序,可以使用二分查找,复杂度为 O(logn)。
数组如何快速的查重?
无序数组通常用 unordered_set 查重,遍历时发现元素已经存在就说明重复,平均时间为O(N)。
如果数组已经有序,比较相邻元素即可,时间O(n)、额外空间O(1)。
如果整数取值范围很小,还可以用计数数组或位图。
double 能不能作为 map 的 key?为什么?
可以,double 能作为 map 的键,因为 map 要求的是比较器满足严格弱序。但要注意浮点精度可能让数学上相等的结果为不同键,还要进行处理,避免破坏排序要求。
请聊一下 map 和 unordered_map
map 和 unordered_map 都保存键唯一的键值对。
map 通常基于红黑树,按比较器顺序存储,查找、普通插入和按键删除为对数复杂度,适合有序遍历和范围查询。
unordered_map 基于哈希表,通过哈希值定位桶,再比较键,查找、插入和删除平均为常数复杂度,最坏为线性复杂度,适合不要求顺序的精确查找。
此外,map 插入不会使已有迭代器失效,而 unordered_map 插入触发重哈希时会使迭代器失效,但元素引用和指针仍然有效。另外,两者的 operator 再键不存在时都会插入元素。
请聊一下 set 和 unordered_set
set 和 unordered_set 都保存不重复键。
set 通常基于红黑树,元素有序,查找和普通增删为对数复杂度,适合有序遍历和范围查询。
unordered_set 基于哈希表,不保证顺序,查找和增删平均为常数复杂度,最坏为线性复杂度,适合去重和存在性查询。
请介绍一下 unordered_map/unordered_set 的哈希机制
unordered_map 底层是哈希表,默认通过 std::hash<Key> 计算键的哈希值,再将哈希值映射到桶。不同键可能进入同一个桶,查找时还要通过相等比较函数确认目标键,不能只判断哈希值。
它用负载因子衡量桶的拥挤程度,插入导致容量不足时会重哈希,增加桶数并重新组织元素。
栈和队列有什么区别?
栈和队列的主要区别是:元素进出的顺序不同。
栈的元素进出顺序是后进先出,插入与删除操作都是在栈顶进行,常用的接口为 push、pop、top。主要的用途为:函数调用、括号匹配、深度优先搜索
队列的元素进出顺序是先进先出,队尾插入,对头删除,常用的接口为:push、pop、front、back。主要用途为:任务排队、消息缓冲、广度优先搜索。
如何用栈来实现队列?如何用队列来实现栈?
两个栈实现队列:一个负责入队,另一个负责出队,输出栈为空时,再把输入栈全部倒过去
用队列实现栈,可以再每次入栈后,把原有元素依次移到队尾,让新元素位于队头;出栈直接删除对头;访问栈顶直接访问队头。
单链表反转如何处理?单链表的环检测如何处理?
单链表反转使用前驱、当前和后继三个指针,先保存后继,再将当前节点指向前驱,依次推进,最后返回新的头结点,时间复杂度为O(n)。
环形检测使用快慢指针,慢指针走一步,快指针走两步,相遇则有环,快指针到空则无环,时间复杂度为 O(n)。