什么是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)的时间复杂度。
- 扩容时需将旧数据拷贝到新内存,时间复杂度为 O(n)(n 为当前元素数)。
均摊分析的核心思想
将多次操作的总成本分摊到每次操作上。虽然扩容操作成本高,但其发生频率随元素数量指数级减少,使得均摊后每次插入的耗时仍为常数。数学推导(以 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插入元素(如
insert
或push_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。
通过旋转(左旋、右旋、双旋)调整平衡,插入/删除后可能触发多次旋转。红黑树:
近似平衡,满足以下规则:
- 节点是红或黑;
- 根节点和叶子节点(NIL)是黑;
- 红节点的子节点必须是黑;
- 每条路径上黑色节点数量相同
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 容器时,优先使用迭代器(如遍历vector
、map
),保证代码通用性和安全性。 - 使用指针:
需要直接操作内存(如自定义数据结构)、与 C 接口交互,或需要多态行为时
哈希表底层实现原理
哈希冲突解决方法
- 链地址法(C++标准库使用):每个桶存链表,插入冲突元素到链表末尾
- 开放寻址法(线性/二次探测):冲突时按规则探测下一个空槽
负载因子与扩容机制
负载因子:哈希桶中元素的数量与哈希桶数量的比值
STL容器在负载因子≥1.0时扩容,新容量通常为原容量的两倍