C++STL容器八股总结

什么是STL

C++的STL是C++编程语言中的一个功能强大且广泛使用的库,它提供了一系列通用的模板类和函数。STL主要包含以下几个组件:

  • 容器(Containers):容器是用来存储和管理数据的数据结构,例如向量(vector)、列表(list)、队列(queue)、栈(stack)、集合(set)和映射(map)等。
  • 迭代器(Iterators):迭代器是一种类似于指针的对象,可以用来遍历容器中的元素。迭代器为容器中的数据提供了统一的访问接口。
  • 算法(Algorithms):STL提供了许多通用的算法,如排序、查找、合并等,这些算法可以直接应用于容器和迭代器。
  • 仿函数(Function Objects):函数对象(仿函数)是实现了函数调用操作符(operator())的类的对象。它们通常用作算法的自定义操作。函数对象有时被称为“仿函数”(functors)。STL还提供了一些预定义的函数对象,例如less、greater、plus等,它们可以用于算法中的比较和运算操作。
  • 适配器(Adapters):适配器是一种特殊的容器或函数对象,它们可以修改或扩展其他容器或函数对象的行为。例如,队列(queue)和栈(stack)就是容器适配器,它们分别基于deque和vector容器实现。函数适配器包括bind、mem_fn等,可以用来组合和修改函数对象。
  • 分配器(Allocators):分配器是一种管理内存分配和释放的机制,它们可以用于自定义容器的内存管理策略。STL中默认使用的分配器是std::allocator,但用户可以根据需要提供自定义的分配器。

vector与list的区别与应用

底层实现

  • vector:vector是基于动态数组实现的,内存空间是连续的。当vector需要扩容时,它会分配一块更大的内存空间,将原有的数据拷贝到新的空间,并释放原有的内存。
  • list:list是基于双向链表实现的,内存空间是非连续的。每个元素都是一个节点,节点之间通过指针相互连接。

性能

  • vector

    • 访问元素:由于内存连续,vector支持随机访问,可以使用下标直接访问任意元素,时间复杂度为O(1)。

    • 插入和删除:在vector的末尾插入和删除元素非常高效,时间复杂度为O(1);但在中间或开头插入和删除元素需要移动后面的元素,时间复杂度为O(n)。

  • list

    • 访问元素:list不支持随机访问,访问任意元素需要从头节点开始遍历链表,时间复杂度为O(n)。
    • 插入和删除:时间复杂度为O(n)。需要遍历链表。

应用场景

  • vector:当需要频繁访问元素,且插入和删除操作主要发生在容器末尾时,vector是一个好的选择。由于其连续内存特性,vector通常更适用于需要高缓存友好性的场景。
  • list:当需要频繁在容器中间或开头进行插入和删除操作,且对随机访问性能要求不高时,list是一个更合适的选择。list也适用于那些不支持或不方便进行内存移动的数据类型的场景,因为list在插入和删除时不会引起元素的内存移动。

vector

1.vector的底层实现

三个指针start_,finish_,end_of_storage

一段连续空间

1.5-2倍的扩容

start_,finish_,end_of_storage

vector是由基于动态数组实现的。开辟在堆上的一块连续的内存空间。

start_是已经存储了数据的开头的起始地址,finish_是存储了数据空间末尾的地址,end_of_storage是内部分配的存储空间的末尾的指针

2.扩容机制

         vector在需要扩容时通常会将容量增加到原来的两倍。这是因为两倍扩容可以在动态数组的插入操作上实现均摊O(1)的时间复杂度。

  1. 扩容时需将旧数据拷贝到新内存,时间复杂度为 O(n)(n 为当前元素数)。
  2. 均摊分析的核心思想
    将多次操作的总成本分摊到每次操作上。虽然扩容操作成本高,但其发生频率随元素数量指数级减少,使得均摊后每次插入的耗时仍为常数。

  3. 数学推导(以 2 倍扩容为例)

    • 扩容次数:插入 n 个元素需扩容 log2​(n) 次。
    • 总拷贝次数:每次扩容拷贝的元素数为 1+2+4+⋯+n/2 ​= n−1。
    • 均摊时间:总时间 O(n) 分摊到 n 次插入,每次均摊时间为 O(1)。

(举个例子,假设每次扩容是2倍。当容量从1开始,插入第2个元素时需要扩容到2,复制1个元素;插入第3个元素时需要扩容到4,复制2个元素;插入第5个元素时扩容到8,复制4个元素,依此类推。每次扩容的复制操作的元素数量是前一次的两倍,但扩容之间的插入次数也是前一次的两倍。因此,总复制次数是1 + 2 + 4 + 8 + ... + n/2,这个等比数列的和大约是n。而总的插入次数是n次,所以均摊到每次插入的时间是O(1)。)

3.迭代器失效

迭代器失效的问题 1. insert扩容引起   2.erase引起。

一、insert扩容引起的迭代器失效

 根本原因与过程

当向vector插入元素(如insertpush_back)时,若当前容量不足,vector会触发扩容操作:

  • 内存重新分配:开辟更大的内存空间(通常容量翻倍);
  • 元素迁移:将旧数据拷贝至新内存,旧空间被释放;
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {1, 2, 3, 4};
    auto it = v.begin();  // 获取起始位置的迭代器
    
    // 插入元素导致扩容(假设初始容量为4,插入第5个元素时触发扩容)
    v.push_back(5);       // 此时迭代器it已失效
    
    cout << *it << endl;  // 访问失效的迭代器,导致未定义行为(崩溃或错误结果)
    return 0;
}

二、erase操作引起的迭代器失效

失效机制

当调用erase删除元素时:

  • 元素前移:删除位置后的元素向前覆盖(若删除中间元素);
  • 边界情况:若删除最后一个元素(如4),pos将指向_finish(vector末尾后的无效位置)
  • 迭代器失效:原迭代器仍指向旧内存地址,成为野指针。

set与unordered_set的区别?

底层数据结构

  • set:基于平衡二叉搜索树(通常是红黑树)实现。元素在容器中按照一定的顺序排列(通过比较运算符 < 或自定义比较函数)。
  • unordered_set:基于哈希表实现。元素在容器中的位置由其哈希值决定,因此元素在容器中的顺序是无序的。

时间复杂度

  • set:插入、删除和查找操作的平均时间复杂度为 O(log n),其中 n 为元素个数。

  • unordered_set:插入、删除和查找操作的平均时间复杂度为 O(1),但在最坏情况下(所有元素都在同一个哈希桶中)时间复杂度可能退化为 O(n)。

空间复杂度

  • set:空间复杂度相对较低,因为它基于平衡二叉搜索树。
  • unordered_set:空间复杂度相对较高,因为哈希表需要分配额外的空间来存储桶和处理冲突。

元素顺序

  • set:元素在容器中按照排序顺序存储,因此遍历 set 时,元素将按照顺序输出。
  • unordered_set:元素在容器中是无序的。遍历时,元素的输出顺序将是随机的。

unordered_map与map的区别?

底层数据结构

  • map:基于平衡二叉搜索树(通常是红黑树)实现。键值对按照键的顺序(通过比较运算符 < 或自定义比较函数)排列。
  • unordered_map:基于哈希表实现。键值对在容器中的位置由键的哈希值决定,因此键值对在容器中的顺序是无序的。

时间复杂度

  • map:插入、删除和查找操作的平均时间复杂度为 O(log n),其中 n 为元素个数。
  • unordered_map:插入、删除和查找操作的平均时间复杂度为 O(1),但在最坏情况下(所有元素都在同一个哈希桶中)时间复杂度可能退化为 O(n)。

空间复杂度

  • map:空间复杂度相对较低,因为它基于平衡二叉搜索树。
  • unordered_map:空间复杂度相对较高,因为哈希表需要分配额外的空间来存储桶和处理冲突。

使用场景

  • map:当需要一个有序容器时,或者当对键值对的插入、删除和查找时间复杂度要求为对数级别时,使用 map 是一个不错的选择。
  • unordered_map:当不需要关心键值对顺序,且需要更快速的插入、删除和查找操作时,使用 unordered_map 更为合适。

AVL树和红黑树

1. 平衡规则
  • AVL树
    严格平衡,任意节点的左右子树高度差(平衡因子)不超过 1
    通过旋转(左旋、右旋、双旋)调整平衡,插入/删除后可能触发多次旋转。

  • 红黑树
    近似平衡,满足以下规则:

    1. 节点是红或黑;
    2. 根节点和叶子节点(NIL)是黑;
    3. 红节点的子节点必须是黑;
    4. 每条路径上黑色节点数量相同
2. 内存开销
  • AVL树
    每个节点需存储平衡因子(通常 2~3 bytes),例如 int balance;
    内存占用略高,但现代系统中差异可忽略。

  • 红黑树
    每个节点需存储颜色标记(1 byte),例如 bool is_red;
    内存更紧凑,适合内存敏感场景。

3. 时间复杂度
操作AVL树红黑树
查找O(log n)O(log n)
插入O(log n)O(log n)
删除O(log n)O(log n)
 
  • AVL树查找更快(严格平衡,路径更短);
  • 红黑树插入/删除更快(旋转次数更少)。

map、set是怎么实现的,红黑树是怎么能够同时实现这两种容器? 为什么使用红黑树?

  • 实现 set:红黑树中的每个节点存储一个键,节点之间通过比较键的大小进行排序。红黑树的节点之间的顺序关系满足二叉搜索树的特性。因此,通过将元素作为红黑树的键,我们可以实现一个 set 容器。
  • 实现 map:为了实现 map 容器,需要同时存储键和值。在红黑树中,每个节点除了存储键之外,还可以存储一个对应的值。通常将键和值封装成一个 pair 对象。这样,红黑树的

迭代器和指针的区别

1.指针直接操作内存地址。迭代器是容器元素的访问方式。

2.迭代器类型很多

  • 随机访问迭代器(如 vector):支持指针式算术运算。
  • 双向迭代器(如 list):仅支持 ++ 和 --

3.何时使用哪种?

  • 优先用迭代器
    操作 STL 容器时,优先使用迭代器(如遍历 vectormap),保证代码通用性和安全性。
  • 使用指针
    需要直接操作内存(如自定义数据结构)、与 C 接口交互,或需要多态行为时

哈希表底层实现原理

哈希冲突解决方法

  1. 链地址法​(C++标准库使用):每个桶存链表,插入冲突元素到链表末尾
  2. 开放寻址法​(线性/二次探测):冲突时按规则探测下一个空槽

负载因子与扩容机制

负载因子:哈希桶中元素的数量与哈希桶数量的比值

STL容器在负载因子≥1.0时扩容,新容量通常为原容量的​​两倍​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值