1. 容器(Containers)
*问题类型:
序列容器(std::vector
, std::deque
, std::list
, std::forward_list
, std::array
, std::string
):
各自的特点、底层实现、优缺点和适用场景?
容器 | 特点 | 底层实现 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|---|
std::vector |
动态数组,支持快速随机访问 | 连续内存 + 三指针(数据头/尾/容量尾) | 随机访问 O(1);缓存友好;尾部操作高效 | 中间插入/删除 O(n);扩容需数据拷贝 | 随机访问为主;尾部增删(如数据缓存) |
std::deque |
双端队列,头尾插入高效 | 分段连续数组 + 中央映射表 | 头尾插入/删除 O(1);支持随机访问 O(1) | 中间插入 O(n);内存碎片化;访问速度略慢于 vector | 队列/栈需双端操作(如任务调度) |
std::list |
双向链表,任意位置插入高效 | 双向链表节点(prev/data/next) | 任意位置插入/删除 O(1);无扩容开销 | 随机访问 O(n);内存占用高;缓存不友好 | 频繁中间增删(如 LRU 缓存) |
std::forward_list |
单向链表,内存更省 | 单向链表节点(data/next) | 内存开销极低(单指针);插入/删除 O(1) | 仅单向遍历;无反向迭代器;操作需前驱节点 | 内存敏感场景(如嵌入式系统链表) |
std::array |
固定大小数组,编译时确定尺寸 | 栈上分配的 C 风格数组封装 | 零内存开销;随机访问 O(1);无动态分配成本 | 大小不可变;无动态扩容 | 替代 C 数组(如固定尺寸矩阵运算) |
std::string |
动态字符串,支持丰富操作 | 类 vector + SSO 优化 | 自动内存管理;内置操作(find/replace 等) | 性能略低于 char*;SSO 有大小限制 | 字符串处理(如文本解析/日志拼接) |
vector
扩容
deque
随机访问
元素位置 = 段起始地址 + 段内偏移
,比 vector
多一次地址跳转。SSO(短字符串优化)
string
对短字符串(通常 ≤15 字符)直接在栈存储,避免堆分配:
std::string s = "Short"; // 栈存储
std::string l = "Long string over 15 chars"; // 堆存储
链表选择指南
list
forward_list
vector/deque
。性能临界场景
array/vector
(缓存友好)forward_list/array
。vector
的扩容机制?为什么是 2 倍或 1.5 倍?
扩容机制:
n
倍),拷贝旧数据到新空间,释放旧内存。扩容因子(1.5或2倍)的原因:
均摊时间复杂度:
O(1)
。数学证明:当因子 k > 1
时,均摊成本为 O(1)
。内存重用:
1.5倍:旧内存块释放后,后续扩容可能重用(新旧内存块大小无重叠)。
例如:释放
size=M
后,新申请1.5M
,后续扩容可能使用之前释放的M
内存。
2倍:释放的内存块总和(M + 2M + 4M + ...
)无法被后续更大的块重用(如 8M
)。
折中策略:
vector
和list
的区别?
特性 | vector |
list |
---|---|---|
底层结构 | 动态数组(连续内存) | 双向链表(非连续内存) |
随机访问 | O(1) (支持下标访问) |
O(n) (需遍历) |
头部插入/删除 | O(n) (移动元素) |
O(1) |
尾部插入/删除 | 均摊 O(1) |
O(1) |
中间插入/删除 | O(n) (移动元素) |
O(1) (定位后操作) |
内存占用 | 较少(仅需存储数据) | 较高(每个节点含两个指针) |
缓存友好性 | 高(连续内存) | 低(内存碎片化) |
扩容开销 | 需重新分配内存和拷贝 | 无扩容(动态分配节点) |
string
和char*
的区别?
特性 | string |
char* |
---|---|---|
内存管理 | 自动分配/释放(RAII) | 手动管理(malloc/free ) |
安全性 | 防越界(自动扩容) | 易缓冲区溢出/内存泄漏 |
功能扩展 | 丰富成员函数(find 、append 等) |
依赖C库函数(strcpy 、strcat ) |
结尾标识 | 可包含 \0 (长度独立管理) |
以 \0 结尾 |
性能开销 | 略高(封装成本) | 极低(直接操作内存) |
兼容性 | C++专用 | 兼容C/C++ |
关键结论:
- 优先使用
string
:安全、便捷,适合大多数场景。- 使用
char*
:需兼容C或极限性能优化(如高频交易系统)。
关联容器(std::set
, std::multiset
, , std::map
, std::multimap
):
各自的特点、底层实现(红黑树?)、优缺点和适用场景
容器 | 特点 | 底层实现 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|---|
std::set |
唯一键集合,自动排序 | 红黑树 | 有序遍历;查找/插入 O(log n) | 内存占用高(每个节点额外信息) | 需有序唯一键(如字典) |
std::multiset |
键可重复,自动排序 | 红黑树 | 支持重复键;有序 | 同 set |
需有序但键可重复(如成绩排名) |
std::map |
键值对(键唯一),按键排序 | 红黑树 | 按键有序;范围查询高效 | 同 set |
键值映射需有序(如学生ID→信息) |
std::multimap |
键值对(键可重复),按键排序 | 红黑树 | 支持重复键;有序 | 同 set |
一键多值(如作者→著作列表) |
map
和unordered_map
的区别?
特性 | std::map |
std::unordered_map |
---|---|---|
底层实现 | 红黑树(平衡二叉搜索树) | 哈希表(桶数组 + 链表/红黑树) |
元素顺序 | 按键有序(升序) | 无序(取决于哈希函数) |
查找复杂度 | O(log n) | 平均 O(1),最坏 O(n) |
内存占用 | 较低(树结构) | 较高(预分配桶 + 链表指针) |
键类型要求 | 需支持 < 或自定义比较器 |
需支持哈希函数和 == 比较 |
适用场景 | 需有序访问/范围查询 | 只需快速查找(如缓存/计数器) |
map
:保证顺序(遍历时按键升序)。unordered_map
:元素顺序不可控(依赖哈希函数)。unordered_map
:平均 O(1) 查找,但哈希冲突时退化(最坏 O(n))。map
:稳定 O(log n),无性能波动。map
(无预分配桶开销)。unordered_map
桶内改用红黑树(如 GCC),最坏复杂度优化至 O(log n)。如何自定义容器的比较函数(std::less
)?
set/map
等)在模板参数中指定自定义比较类型:
// 方法1:函数对象(推荐)
struct CustomCompare {
bool operator()(const T& a, const T& b) const {
return /* 自定义逻辑 */; // 例:a.salary > b.salary
}
};
std::set<T, CustomCompare> mySet;
// 方法2:Lambda(C++11+)
auto comp = [](const T& a, const T& b) { /* ... */ };
std::set<T, decltype(comp)> mySet(comp); // 需传递Lambda对象
vector/list
等)在排序操作时传入比较函数:
std::vector<T> vec;
// 方法1:Lambda表达式
std::sort(vec.begin(), vec.end(), [](const T& a, const T& b) {
return a.id < b.id; // 按ID升序
});
// 方法2:函数指针
bool compareFunc(const T& a, const T& b) { /* ... */ }
std::sort(vec.begin(), vec.end(), compareFunc);
priority_queue
)在模板参数中指定比较类型:
// 小顶堆示例
struct Greater {
bool operator()(int a, int b) const {
return a > b; // 小值优先
}
};
std::priority_queue<int, std::vector<int>, Greater> minHeap;
关键规则
需满足:
!comp(a, a) // 非自反
comp(a, b) => !comp(b, a) // 非对称
comp(a, b) && comp(b, c) => comp(a, c) // 传递性
违反示例(错误):
[](int a, int b) { return a <= b; } // 包含相等,违反非自反性
operator()
且为 const
成员函数无序关联容器(std::unordered_set
, std::unordered_multiset
, std::unordered_map
, std::unordered_multimap
):
各自的特点、底层实现(哈希表)、优缺点和适用场景?
容器 | 特点 | 底层实现 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|---|
std::unordered_set |
唯一键集合,无序存储 | 哈希表(桶+链表/红黑树) | 平均 O(1) 查找/插入 | 最坏 O(n);内存占用高 | 快速去重(如URL黑名单) |
std::unordered_multiset |
键可重复,无序存储 | 同上 | 支持重复键 | 同 unordered_set |
词频统计(如单词计数) |
std::unordered_map |
键值对(键唯一),无序存储 | 同上 | 平均 O(1) 键值访问 | 同 unordered_set |
高速键值查询(如缓存) |
std::unordered_multimap |
键值对(键可重复),无序存储 | 同上 | 支持一键多值 | 同 unordered_set |
多值映射(如电话簿) |
底层实现核心:
- 哈希表 = 桶数组(连续内存) + 冲突解决结构(链表/红黑树)
- C++11 优化:桶内元素超阈值(如 GCC 为 8)时,链表转红黑树(最坏复杂度优化至 O(log n))
哈希冲突的解决方法?
方法 | 原理 | 实现示例 | 特点 |
---|---|---|---|
链地址法 | 桶内挂链表存储冲突元素 | bucket[i] = head→a→b→null |
简单通用;C++标准库默认方案 |
开放寻址法 | 线性探测:冲突时找下一个空桶 | bucket[(hash+1)%size] |
缓存友好;需负载因子控制 |
罗宾汉哈希 | 冲突时比较探测距离,抢占更远槽位 | 复杂优化 | 减少最长探测距离 |
关键参数:
- 负载因子 = 元素数 / 桶数(默认 1.0)
- 扩容触发:负载因子 >
max_load_factor()
时,桶数翻倍并重哈希
如何自定义容器的比较函数?
需定义 两个函数对象:
// 示例:自定义Point类型作为键
struct Point { int x; int y; };
// 1. 自定义哈希函数
struct PointHash {
size_t operator()(const Point& p) const {
return std::hash<int>()(p.x) ^ (std::hash<int>()(p.y) << 1);
}
};
// 2. 自定义键相等比较
struct PointEqual {
bool operator()(const Point& a, const Point& b) const {
return a.x == b.x && a.y == b.y;
}
};
// 定义容器
std::unordered_map<Point, std::string, PointHash, PointEqual> pointMap;
关键规则:
哈希一致性:若
a == b
,则hash(a) == hash(b)
性能要求:哈希函数需高效(O(1) 复杂度)
特化
std::hash
(可选):namespace std { template<> struct hash<Point> { /* ... */ }; }
容器配合(std::stack
, std::queue
, , std::priority_queue
):
各自的特点、底层实现、优缺点和适用场景?
适配器 | 特点 | 默认底层容器 | 核心操作 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|---|---|
std::stack |
后进先出(LIFO) | std::deque |
push() , pop() , top() |
操作简单高效(O(1)) | 只能访问顶部元素 | 函数调用栈/撤销操作/括号匹配 |
std::queue |
先进先出(FIFO) | std::deque |
push() , pop() , front() |
头尾操作高效(O(1)) | 只能访问两端元素 | 消息队列/缓冲区/广度优先搜索 |
std::priority_queue |
优先级队列(最大堆) | std::vector |
push() , pop() , top() |
高效获取极值(O(1)) | 插入慢(O(log n)) | 任务调度/事件处理/Dijkstra算法 |
底层实现详解
stack
& queue
默认容器选择使用 deque
(双端队列)原因:
两端插入/删除均为 O(1)
内存自动扩展(避免 vector
扩容时的全量拷贝)
示例:
std::stack<int> s; // 等价于 std::stack>
std::queue<char> q; // 等价于 std::queue>
priority_queue
实现原理基于 堆结构(完全二叉树)
底层容器需支持:
operator[]
)→ 故用 vector
push_back()
, pop_back()
)堆操作:
// 插入元素(上浮)
vec.push_back(x);
std::push_heap(vec.begin(), vec.end());
// 删除顶部(下沉)
std::pop_heap(vec.begin(), vec.end());
vec.pop_back();
自定义底层容器
// 使用 vector 作为 stack 的底层容器
std::stack<int, std::vector<int>> vecStack;
// 使用 list 作为 queue 的底层容器
std::queue<int, std::list<int>> listQueue;
// 使用 deque 作为 priority_queue 的底层容器
std::priority_queue<int, std::deque<int>> dequePQ;
注意限制:
stack
:需支持push_back()
,pop_back()
,back()
queue
:需支持push_back()
,pop_front()
,front()
priority_queue
:需支持随机访问迭代器 +front()
性能与选择指南
stack
/queue
vs 原生容器deque
priority_queue
优化自定义比较器实现最小堆:
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
复杂类型场景:
struct Task { int priority; /*...*/ };
auto comp = [](const Task& a, const Task& b) {
return a.priority < b.priority;
};
std::priority_queue<Task, std::vector<Task>, decltype(comp)> taskQueue(comp);
固定大小栈 → 用 array
替代 deque
:
std::stack<int, std::array<int, 100>> fixedStack;
2. 修改算法(算法)
*问题类型:
常用的非式修改序列操作(std::for_each
, std::find
,std::count
等)?
算法 | 功能 | 示例 |
---|---|---|
std::for_each |
对每个元素执行操作 | for_each(v.begin(), v.end(), print) |
std::find |
查找首个匹配元素 | find(v.begin(), v.end(), 42) |
std::find_if |
查找首个满足条件的元素 | find_if(v.begin(), v.end(), isEven) |
std::count |
统计匹配元素数量 | count(v.begin(), v.end(), 'a') |
std::count_if |
统计满足条件的元素数量 | count_if(v.begin(), v.end(), isPrime) |
std::all_of |
检查所有元素满足条件 (C++11) | all_of(v.begin(), v.end(), isPositive) |
std::any_of |
检查至少一个元素满足条件 (C++11) | any_of(v.begin(), v.end(), isNegative) |
常用的非式序列操作(std::copy
, std::remove
, std::sort
, std::unique
,std::reverse
等)?
算法 | 功能 | 注意要点 |
---|---|---|
std::copy |
复制序列到目标位置 | 目标容器需预分配空间 |
std::remove |
逻辑删除匹配元素(移动元素) | 需配合 erase 物理删除(见示例↓) |
std::remove_if |
逻辑删除满足条件的元素 | 同上 |
std::sort |
排序(默认升序) | 需随机访问迭代器(不支持 list ) |
std::stable_sort |
稳定排序(保持相等元素顺序) | 性能略低于 sort |
std::unique |
删除连续重复元素 | 需先排序;返回新逻辑终点 |
std::reverse |
反转序列元素顺序 | list 有成员函数 reverse() |
删除元素标准写法:
// vector 删除偶数
auto newEnd = remove_if(vec.begin(), vec.end(), isEven);
vec.erase(newEnd, vec.end()); // 物理删除
常用的数值算法(std::accumulate
等)?
算法 | 功能 | 示例 |
---|---|---|
std::accumulate |
累加/自定义归约操作 | accumulate(v.begin(), v.end(), 0) |
std::inner_product |
计算内积(点积) | inner_product(a.begin(), a.end(), b.begin(), 0) |
std::partial_sum |
生成前缀和序列 | partial_sum(v.begin(), v.end(), out) |
std::iota |
填充递增序列 (C++11) | iota(v.begin(), v.end(), 10) // 10,11,12… |
如何使用这些算法以及它们与容器成员函数的区别?
场景 | 选择 | 原因 |
---|---|---|
list 排序 |
成员 sort() |
算法 std::sort 需随机访问(不支持链表) |
set 查找 |
成员 find() |
算法 std::find 是 O(n),成员是 O(log n) |
关联容器删除 | 成员 erase() |
算法 std::remove 破坏树结构 |
通用序列操作 | STL 算法 | 统一接口,可跨容器使用 |
关键原则:
- 关联容器/链表:优先用成员函数(性能/正确性)
- 序列容器(
vector/deque
):优先 STL 算法(通用性)
std::sort
底层实现?(通常是 Introsort)
混合排序策略:
核心优势:
示例代码:
std::vector<int> v = {5, 3, 2, 8, 1};
std::sort(v.begin(), v.end()); // 升序
std::sort(v.begin(), v.end(), std::greater<int>()); // 降序
3. 迭代器(Iterators)
*问题类型:
迭代器的概念和?
*
、->
、++
等操作)。std::sort
可作用于所有支持随机访问迭代器的容器)五种迭代器作用类别(输入、输出、前向、个体、随机访问)及其特性?
类别 | 支持操作 | 容器示例 |
---|---|---|
输入迭代器 | 只读单次遍历 (*it , ++ , == , != ) |
istream_iterator |
输出迭代器 | 只写单次遍历 (*it= , ++ ) |
ostream_iterator |
前向迭代器 | 读写 + 多次遍历(继承输入/输出) | forward_list , unordered_* |
双向迭代器 | 前向 + 反向遍历 (-- ) |
list , set , map |
随机访问迭代器 | 双向 + 跳跃访问 (it+n , it[n] , < ) |
vector , deque , array |
层级关系:
输入 → 前向 → 双向 → 随机访问
输出 → 前向 → …
begin()
, end()
, cbegin()
, cend()
, rbegin()
,rend()
的区别?
函数 | 返回类型 | 方向 | 可修改性 | 容器示例调用 |
---|---|---|---|---|
begin() |
普通迭代器 | 正向 | 可读写 | vec.begin() |
end() |
普通迭代器 | 正向尾后 | 不可解引用 | vec.end() |
cbegin() |
const 迭代器 | 正向 | 只读 | vec.cbegin() |
cend() |
const 迭代器 | 正向尾后 | 不可解引用 | vec.cend() |
rbegin() |
反向迭代器 | 反向 | 可读写 | vec.rbegin() |
rend() |
反向迭代器 | 反向尾后 | 不可解引用 | vec.rend() |
crbegin() |
const 反向迭代器 | 反向 | 只读 | vec.crbegin() |
反向迭代器注意:
std::vector<int> v = {1,2,3}; auto rit = v.rbegin(); // 指向 3 *rit = 4; // v = {1,2,4}
迭代器故障问题,何时会发生,如何避免?
何时发生:
容器 | 导致失效的操作 |
---|---|
vector /string |
插入/删除(导致扩容或元素移动) |
deque |
首尾外插入/删除、扩容 |
list /forward_list |
仅删除时使被删元素迭代器失效 |
关联容器 | 仅删除时使被删元素迭代器失效 |
避免方法:
插入后更新迭代器:
auto it = vec.begin();
it = vec.insert(it, 10); // 获取新迭代器
删除时用返回值更新:
for (auto it = lst.begin(); it != lst.end(); ) {
if (*it % 2 == 0) it = lst.erase(it); // 更新为下一个元素
else ++it;
}
避免失效区间操作:
// 错误:删除导致后续迭代器失效
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (cond) vec.erase(it); // UB!
}
关键原则:
- 序列容器(
vector/deque
)修改后所有迭代器可能失效- 链表/关联容器修改后仅被删元素迭代器失效
4. 函数对象 (Functors) / Lambda 表达式
*问题类型:
函数对象的概念和作用?
概念:重载了 operator()
的类对象,可像函数一样调用
作用:
示例:
struct Compare {
bool operator()(int a, int b) const {
return a > b; // 实现自定义比较
}
};
std::sort(v.begin(), v.end(), Compare());
Lambda 表达式的语法和捕获列表(Capture List)?
语法:
[捕获列表](参数) -> 返回类型 { 函数体 }
捕获列表(Capture List):
捕获方式 | 效果 |
---|---|
[] |
不捕获外部变量 |
[x] |
按值捕获变量 x (副本) |
[&x] |
按引用捕获变量 x |
[=] |
按值捕获所有外部变量(不推荐!) |
[&] |
按引用捕获所有外部变量(不推荐!) |
[this] |
捕获当前类对象的 this 指针 |
[x = expr] |
C++14:用表达式初始化捕获(移动捕获) |
示例:
int base = 100;
auto add = [base](int x) { return x + base; };
std::cout << add(5); // 输出 105
Lambda 表达式的本质?
编译器行为:
将 Lambda 转换为匿名函数对象类(含重载的 operator()
)
转换示例:
// Lambda: [](int x) { return x * 2; }
// 编译器生成类似:
class __Lambda_123 {
public:
int operator()(int x) const { return x * 2; }
};
什么时候使用函数对象或 Lambda 表达式?
场景 | 推荐方式 | 原因 |
---|---|---|
简单逻辑(如比较器) | Lambda | 代码简洁,无需额外定义 |
需要携带复杂状态 | 函数对象 | 可定义多个成员变量/辅助方法 |
需要递归调用 | 函数对象 | Lambda 递归需 std::function (性能损失) |
需作为模板参数(如容器的比较器) | 函数对象或无捕获 Lambda | 无捕获 Lambda 可转换为函数指针 |
需复用逻辑 | 函数对象 | 避免重复定义 |
关键区别:
- 函数对象:显式定义类型,适合复杂/复用逻辑
- Lambda:隐式匿名类型,适合一次性简单逻辑