1.容器:vector,list,map,set的底层实现(数组、链 表、红黑树)及时间复杂度
1. 容器分类概览
序列式容器 vs 关联式容器
// 序列式容器:元素顺序由插入顺序决定
std::vector<int> vec; // 动态数组
std::list<int> lst; // 双向链表
std::deque<int> deq; // 双端队列
// 关联式容器:元素按特定顺序存储
std::set<int> s; // 有序集合(红黑树)
std::map<int, std::string> m; // 有序映射(红黑树)
std::unordered_set<int> us; // 哈希集合
std::unordered_map<int, std::string> um; // 哈希映射
2. std::vector - 动态数组
底层实现
// vector的简化内存布局
template<typename T>
class vector {
private:
T* start; // 指向首元素
T* finish; // 指向最后一个元素的下一个位置
T* end_of_storage; // 指向分配内存的末尾
};
内存增长策略:当容量不足时,通常按2倍或1.5倍扩容,分配新内存,拷贝元素,释放旧内存。
时间复杂度分析
操作 | 时间复杂度 | 说明 |
---|---|---|
访问 | ||
operator[] , at() , front() , back() |
O(1) | 随机访问,直接通过指针算术 |
插入 | ||
push_back() (平均) |
O(1) | 分摊常数时间(考虑扩容) |
push_back() (最坏) |
O(n) | 需要扩容和拷贝所有元素 |
insert() (任意位置) |
O(n) | 需要移动后续元素 |
删除 | ||
pop_back() |
O(1) | 只需调整指针 |
erase() (任意位置) |
O(n) | 需要移动后续元素 |
查找 | ||
find() |
O(n) | 线性搜索 |
代码示例
#include <vector>
#include <iostream>
void vector_example() {
std::vector<int> vec;
// 插入 - 分摊O(1)
for (int i = 0; i < 10; ++i) {
vec.push_back(i); // 可能触发扩容
}
// 随机访问 - O(1)
std::cout << "Element at index 5: " << vec[5] << std::endl;
// 中间插入 - O(n)
vec.insert(vec.begin() + 3, 100); // 需要移动后面所有元素
// 删除中间元素 - O(n)
vec.erase(vec.begin() + 3); // 需要移动后面所有元素
}
3. std::list - 双向链表
底层实现
// list节点的简化结构
template<typename T>
struct list_node {
T data;
list_node* prev;
list_node* next;
};
// list的简化结构
template<typename T>
class list {
private:
list_node<T>* head; // 头节点(通常包含双向循环)
size_t size;
};
时间复杂度分析
操作 | 时间复杂度 | 说明 |
---|---|---|
访问 | ||
front() , back() |
O(1) | 直接访问头尾 |
任意位置访问 | O(n) | 需要遍历链表 |
插入 | ||
push_front() , push_back() |
O(1) | 直接修改指针 |
insert() (已知位置) |
O(1) | 只需修改指针 |
删除 | ||
pop_front() , pop_back() |
O(1) | 直接修改指针 |
erase() (已知位置) |
O(1) | 只需修改指针 |
查找 | ||
find() |
O(n) | 需要遍历链表 |
代码示例
#include <list>
#include <iostream>
void list_example() {
std::list<int> lst = {1, 2, 3, 4, 5};
// 头尾插入 - O(1)
lst.push_front(0);
lst.push_back(6);
// 中间插入 - O(1)(已知迭代器位置)
auto it = lst.begin();
std::advance(it, 3); // 移动到第4个元素 - O(n)
lst.insert(it, 100); // 插入 - O(1)
// 删除 - O(1)(已知迭代器位置)
it = lst.begin();
std::advance(it, 2); // O(n)
lst.erase(it); // O(1)
// 查找 - O(n)
auto found = std::find(lst.begin(), lst.end(), 4);
if (found != lst.end()) {
std::cout << "Found: " << *found << std::endl;
}
}
4. std::map - 有序映射(红黑树)
底层实现:红黑树
红黑树是一种自平衡的二叉搜索树,满足:
每个节点是红色或黑色
根节点是黑色
所有叶子节点(NIL)是黑色
红色节点的子节点必须是黑色
从任一节点到其每个叶子的所有路径包含相同数目的黑色节点
时间复杂度分析
操作 | 时间复杂度 | 说明 |
---|---|---|
访问 | ||
operator[] , at() |
O(log n) | 二叉搜索树查找 |
插入 | ||
insert() |
O(log n) | 查找位置 + 平衡调整 |
删除 | ||
erase() |
O(log n) | 查找位置 + 平衡调整 |
查找 | ||
find() |
O(log n) | 二叉搜索树查找 |
遍历 | ||
有序遍历 | O(n) | 中序遍历 |
代码示例
#include <map>
#include <iostream>
void map_example() {
std::map<int, std::string> student_map;
// 插入 - O(log n)
student_map.insert({1, "Alice"});
student_map[2] = "Bob"; // O(log n)
student_map.emplace(3, "Charlie"); // O(log n)
// 查找 - O(log n)
auto it = student_map.find(2);
if (it != student_map.end()) {
std::cout << "Found: " << it->second << std::endl;
}
// 访问 - O(log n)
std::cout << "Student 1: " << student_map.at(1) << std::endl;
// 删除 - O(log n)
student_map.erase(3);
// 有序遍历 - O(n)
for (const auto& [id, name] : student_map) {
std::cout << id << ": " << name << std::endl;
}
}
5. std::set - 有序集合(红黑树)
底层实现
std::set
的底层实现也是红黑树,但只存储键(没有值)。
时间复杂度分析
操作 | 时间复杂度 | 说明 |
---|---|---|
插入 | O(log n) | 查找位置 + 平衡调整 |
删除 | O(log n) | 查找位置 + 平衡调整 |
查找 | O(log n) | 二叉搜索树查找 |
遍历 | O(n) | 中序遍历 |
#include <set>
#include <iostream>
void set_example() {
std::set<int> unique_numbers;
// 插入 - O(log n)
unique_numbers.insert(5);
unique_numbers.insert(2);
unique_numbers.insert(8);
unique_numbers.insert(2); // 重复,不会插入
// 查找 - O(log n)
if (unique_numbers.find(5) != unique_numbers.end()) {
std::cout << "5 exists" << std::endl;
}
// 删除 - O(log n)
unique_numbers.erase(2);
// 有序遍历 - O(n)
for (int num : unique_numbers) {
std::cout << num << " "; // 输出: 5 8
}
std::cout << std::endl;
}
6. 综合对比表
容器 | 底层结构 | 访问 | 插入 | 删除 | 查找 | 有序 | 重复元素 |
---|---|---|---|---|---|---|---|
vector | 动态数组 | O(1) | 尾部O(1)* | 尾部O(1) | O(n) | 插入序 | 允许 |
list | 双向链表 | O(n) | O(1) | O(1) | O(n) | 插入序 | 允许 |
map | 红黑树 | O(log n) | O(log n) | O(log n) | O(log n) | 键序 | 键唯一 |
set | 红黑树 | - | O(log n) | O(log n) | O(log n) | 键序 | 元素唯一 |
*注:vector的push_back()是分摊O(1),最坏情况O(n)
7. 常见问题
Q1: vector的扩容策略是什么?
答:通常按2倍或1.5倍扩容。当容量不足时,分配新内存(通常是原来的2倍),拷贝所有元素到新内存,释放旧内存。扩容操作的时间复杂度是O(n)。
Q2: 为什么vector的push_back()是分摊O(1)?
答:虽然单次扩容是O(n),但经过数学证明,n次push_back()操作的总时间复杂度是O(n),因此单次操作的分摊时间复杂度是O(1)。
Q3: list和vector如何选择?
答:
需要随机访问:选择vector (O(1))
需要频繁在中间插入删除:选择list (O(1))
内存连续性重要:选择vector
需要大量头尾操作:都可以,但list的push_front也是O(1)
Q4: 红黑树相比AVL树有什么优势?
答:红黑树的平衡要求比AVL树宽松,插入删除时需要更少的旋转操作,因此在实际应用中性能更好,特别是写入操作频繁的场景。
Q5: map和unordered_map如何选择?
答:
需要元素有序:选择map (红黑树)
需要最快查找速度:选择unordered_map (哈希表,平均O(1))
需要内存紧凑:选择map (红黑树更节省内存)
需要频繁插入删除:两者都是O(log n) / O(1),但unordered_map常数因子更小
总结
理解STL容器的底层实现至关重要:
✅ vector:动态数组,随机访问快,中间操作慢
✅ list:双向链表,插入删除快,随机访问慢
✅ map/set:红黑树,有序,查找插入删除都是O(log n)
✅ 选择策略:根据具体需求选择最合适的容器
✅ 性能特征:不同操作的时间复杂度差异很大
2.迭代器:种类及失效问题
1. 迭代器基本概念
什么是迭代器?
迭代器是STL中用于遍历容器元素的对象,它提供了统一的访问接口,使得算法可以独立于容器类型工作。
#include <vector>
#include <list>
#include <iostream>
void iterator_basics() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<int> lst = {1, 2, 3, 4, 5};
// 使用迭代器遍历vector
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// 使用迭代器遍历list(接口相同!)
for (auto it = lst.begin(); it != lst.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// 这就是STL的设计哲学:算法与容器分离
}
2. 迭代器的五种类型
1. 输入迭代器(Input Iterator)
特性:只读,单向,只能递增
// 典型应用:从输入流读取数据
#include <iterator>
#include <iostream>
void input_iterator_example() {
std::istream_iterator<int> input_it(std::cin);
std::istream_iterator<int> end;
while (input_it != end) {
std::cout << "Read: " << *input_it << std::endl;
++input_it; // 只能向前,不能回头
}
}
2. 输出迭代器(Output Iterator)
特性:只写,单向,只能递增
// 典型应用:向输出流写入数据
#include <iterator>
#include <vector>
void output_iterator_example() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::ostream_iterator<int> output_it(std::cout, " ");
for (int num : vec) {
*output_it = num; // 只能写入,不能读取
++output_it;
}
}
3. 前向迭代器(Forward Iterator)
特性:可读写,单向,可多次遍历
// 典型应用:单链表(std::forward_list)
#include <forward_list>
void forward_iterator_example() {
std::forward_list<int> flist = {1, 2, 3, 4, 5};
auto it = flist.begin();
std::cout << *it << std::endl; // 读取
*it = 100; // 写入
++it; // 只能向前
// 可以多次遍历
for (auto it = flist.begin(); it != flist.end(); ++it) {
std::cout << *it << " ";
}
}
4. 双向迭代器(Bidirectional Iterator)
特性:可读写,可向前向后移动
// 典型应用:双向链表(std::list)
#include <list>
void bidirectional_iterator_example() {
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = lst.begin();
++it; // 向前
--it; // 向后
// 反向遍历
for (auto rit = lst.rbegin(); rit != lst.rend(); ++rit) {
std::cout << *rit << " ";
}
}
5. 随机访问迭代器(Random Access Iterator)
特性:可读写,支持随机访问,算术运算
// 典型应用:数组、vector、deque
#include <vector>
void random_access_iterator_example() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin();
it += 3; // 随机访问
std::cout << it[1] << endl; // 支持下标操作
std::cout << it - vec.begin() << endl; // 计算距离
// 支持比较操作
if (it > vec.begin()) {
std::cout << "it is after begin" << std::endl;
}
}
迭代器类别总结表
迭代器类型 | 支持操作 | 典型容器 |
---|---|---|
输入迭代器 | 只读,++,==,!= | istream |
输出迭代器 | 只写,++ | ostream |
前向迭代器 | 读写,++ | forward_list |
双向迭代器 | 读写,++,-- | list, set, map |
随机访问迭代器 | 读写,++,--,+,-,[] | vector, deque, array |
3. 迭代器失效问题
什么是迭代器失效?
当容器结构发生变化(插入、删除元素)时,指向容器元素的迭代器可能变得无效,继续使用会导致未定义行为。
1. vector的迭代器失效
void vector_iterator_invalidation() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin() + 2; // 指向3
// 情况1:插入元素可能导致重新分配
for (int i = 0; i < 100; ++i) {
vec.push_back(i); // 可能触发扩容
// 此时it可能失效!
}
// std::cout << *it << std::endl; // 危险!未定义行为
// 情况2:删除元素
it = vec.begin() + 2; // 重新获取
vec.erase(vec.begin() + 1); // 删除第2个元素
// it现在指向什么?可能失效!
// 正确做法:使用erase的返回值
it = vec.erase(it); // it现在指向被删除元素的下一个元素
std::cout << *it << std::endl; // 安全
}
1. vector的迭代器失效
void vector_iterator_invalidation() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin() + 2; // 指向3
// 情况1:插入元素可能导致重新分配
for (int i = 0; i < 100; ++i) {
vec.push_back(i); // 可能触发扩容
// 此时it可能失效!
}
// std::cout << *it << std::endl; // 危险!未定义行为
// 情况2:删除元素
it = vec.begin() + 2; // 重新获取
vec.erase(vec.begin() + 1); // 删除第2个元素
// it现在指向什么?可能失效!
// 正确做法:使用erase的返回值
it = vec.erase(it); // it现在指向被删除元素的下一个元素
std::cout << *it << std::endl; // 安全
}
2. list的迭代器失效
void list_iterator_invalidation() {
std::list<int> lst = {1, 2, 3, 4, 5};
auto it = ++lst.begin(); // 指向2
// list的插入不会使其他迭代器失效
lst.insert(lst.begin(), 0); // 插入开头
std::cout << *it << std::endl; // 安全:仍然指向2
// 但删除会使被删除元素的迭代器失效
auto to_erase = it;
++it; // 先移动下一个迭代器
lst.erase(to_erase); // 删除元素2
std::cout << *it << std::endl; // 安全:指向3
}
3. map/set的迭代器失效
void map_iterator_invalidation() {
std::map<int, std::string> map = {{1, "a"}, {2, "b"}, {3, "c"}};
auto it = map.find(2);
// 插入不会使迭代器失效
map.insert({4, "d"});
std::cout << it->second << std::endl; // 安全:仍然指向"b"
// 删除会使被删除元素的迭代器失效
auto to_erase = it;
++it; // 先移动到下一个元素
map.erase(to_erase); // 删除键为2的元素
std::cout << it->second << std::endl; // 安全:指向"c"
}
4. deque的迭代器失效
void deque_iterator_invalidation() {
std::deque<int> deq = {1, 2, 3, 4, 5};
auto it = deq.begin() + 2; // 指向3
// 在头尾插入通常不会使迭代器失效
deq.push_front(0);
deq.push_back(6);
std::cout << *it << std::endl; // 通常安全:仍然指向3
// 在中间插入可能使所有迭代器失效
deq.insert(deq.begin() + 2, 100);
// std::cout << *it << std::endl; // 危险!可能失效
}
4. 迭代器失效规则总结
各容器迭代器失效规则
容器 | 插入操作 | 删除操作 |
---|---|---|
vector | 可能所有迭代器失效(扩容时) | 被删元素及之后迭代器失效 |
deque | 中间插入:所有迭代器可能失效 头尾插入:通常不会失效 |
中间删除:所有迭代器可能失效 头尾删除:通常只使被删迭代器失效 |
list | 不会使任何迭代器失效 | 只使被删元素的迭代器失效 |
map/set | 不会使任何迭代器失效 | 只使被删元素的迭代器失效 |
unordered_ map/set |
可能所有迭代器失效(重哈希时) | 只使被删元素的迭代器失效 |
安全使用准则
void safe_iterator_usage() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 错误示范:在遍历时修改容器
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 3) {
vec.erase(it); // 错误!it失效后继续使用
// 应该:it = vec.erase(it); 然后 continue;
}
}
// 正确做法1:使用返回值更新迭代器
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it == 3) {
it = vec.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
// 正确做法2:使用算法remove-erase惯用法
vec.erase(std::remove(vec.begin(), vec.end(), 3), vec.end());
}
5. 特殊迭代器
反向迭代器(Reverse Iterator)
void reverse_iterator_example() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 反向遍历
for (auto rit = vec.rbegin(); rit != vec.rend(); ++rit) {
std::cout << *rit << " "; // 输出: 5 4 3 2 1
}
// 反向迭代器与普通迭代器的转换
auto rit = vec.rbegin();
auto it = rit.base(); // 获取对应的正向迭代器
std::cout << *it << std::endl; // 输出rend位置的元素
}
插入迭代器(Insert Iterator)
void insert_iterator_example() {
std::vector<int> vec = {1, 2, 3};
std::list<int> lst = {4, 5, 6};
// 使用插入迭代器拷贝元素
std::back_insert_iterator<std::vector<int>> back_inserter(vec);
std::copy(lst.begin(), lst.end(), back_inserter);
// vec现在: {1, 2, 3, 4, 5, 6}
// 简便写法
std::copy(lst.begin(), lst.end(), std::back_inserter(vec));
}
6. 常见问题
Q1: 什么是迭代器失效?举例说明
答:迭代器失效是指当容器结构发生变化时,之前获取的迭代器不再指向有效的元素。例如在vector中插入元素可能导致扩容,所有迭代器都失效。
Q2: 如何安全地在遍历时删除元素?
答:
vector/deque:使用
it = vec.erase(it)
并检查返回值list/map/set:先保存下一个迭代器
next = it++;
再删除或者使用 remove-erase惯用法
Q3: 不同容器的迭代器类别是什么?
答:
vector, deque, array:随机访问迭代器
list, map, set:双向迭代器
forward_list:前向迭代器
unordered容器:前向迭代器
Q4: 什么是迭代器 trait?
答:迭代器trait是用于在编译期获取迭代器特性的模板类,可以获取迭代器的类别、值类型、差异类型等信息。
Q5: 如何实现自定义迭代器?
答:需要定义相应的iterator_category、value_type、difference_type、pointer、reference类型,并实现相应的操作符重载。
7. 最佳实践
使用算法避免手动迭代
void use_algorithms() {
std::vector<int> vec = {1, 2, 3, 4, 5, 3, 2, 1};
// 而不是手动遍历删除
vec.erase(std::remove(vec.begin(), vec.end(), 3), vec.end());
// 使用算法处理
std::sort(vec.begin(), vec.end());
auto last = std::unique(vec.begin(), vec.end());
vec.erase(last, vec.end());
}
谨慎保存迭代器
void be_careful_with_iterators() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 危险:保存迭代器
auto saved_it = vec.begin() + 2;
// 中间操作可能使迭代器失效
for (int i = 0; i < 100; ++i) {
vec.push_back(i);
}
// std::cout << *saved_it << std::endl; // 未定义行为!
// 解决方案:保存索引而不是迭代器
size_t index = 2;
// ... 各种操作
std::cout << vec[index] << std::endl; // 安全
}
总结
理解迭代器及其失效机制至关重要:
✅ 迭代器类别:了解不同迭代器的能力和限制
✅ 失效规则:掌握各容器在修改操作后的迭代器状态
✅ 安全实践:使用正确的方法在遍历时修改容器
✅ 算法优先:尽量使用STL算法而不是手动迭代
3.算法:find,sort,copy等
//TODO