目录
3. vector和list都有缺点,有什么其他的设计方案, 能解决问题?
8. unordered_map和unordered_set是如何实现的?
10. 一个类型做unordered_map和unordered_set有什 么要求?
一、STL概述
(一)序列式容器
1. vector(基于动态数组)
std::vector 是一个动态数组,支持随机访问,尾部插入和删除操作效率高。
示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
vec.push_back(6); // 尾部插入
vec.insert(vec.begin() + 2, 10); // 在指定位置插入
vec.erase(vec.begin() + 2); // 删除指定位置的元素
for (int x : vec) {
std::cout << x << " ";
}
return 0;
}
底层实现是动态数组:内存连续,支持随机访问。
扩容机制:
当 vector 的容量不足以容纳新元素时,会分配一块更大的内存(通常是当前容量的两倍)。
将现有元素拷贝到新内存中,然后释放旧内存。
优点:
随机访问效率高(O(1))。
尾部插入和删除效率高(平均 O(1))。
内存连续,缓存友好。
缺点:
插入和删除中间元素效率低(O(n))。
扩容时需要重新分配内存并拷贝数据。
2. list(基于双向链表)
std::list 是一个双向链表,支持在任意位置高效地插入和删除元素。
示例:
#include <iostream>
#include <list>
int main() {
std::list<int> lst = {1, 2, 3, 4, 5};
lst.push_back(6); // 尾部插入
lst.push_front(0); // 头部插入
lst.insert(std::next(lst.begin(), 2), 10); // 在指定位置插入
lst.erase(std::next(lst.begin(), 2)); // 删除指定位置的元素
for (int x : lst) {
std::cout << x << " ";
}
return 0;
}
底层实现是双向链表:每个节点包含数据和前后指针。
内存分配:每个节点单独分配内存,内存不连续。
优点:
插入和删除任意位置的元素效率高(O(1))。
双向遍历。
缺点:
随机访问效率低(O(n))。
内存占用较大(每个节点包含指针)。
3. deque(基于分段的动态数组)
std::deque 是一个双端队列,支持在两端高效地插入和删除元素。
示例:
#include <iostream>
#include <deque>
int main() {
std::deque<int> dq = {1, 2, 3, 4, 5};
dq.push_back(6); // 尾部插入
dq.push_front(0); // 头部插入
dq.insert(dq.begin() + 2, 10); // 在指定位置插入
dq.erase(dq.begin() + 2); // 删除指定位置的元素
for (int x : dq) {
std::cout << x << " ";
}
return 0;
}
底层实现是分段的动态数组:内存分段管理,每段是一个固定大小的数组。
内存分配:动态分配内存,支持两端扩展。
优点:
两端操作效率高(O(1))。
支持随机访问(O(1))。
缺点:
内存分配较为复杂,可能涉及多次内存分配。
随机访问效率低于 vector。
4. string(基于动态数组)
std::string 是一个动态数组,用于存储字符序列,支持随机访问和动态扩展。
示例:
#include <iostream>
#include <string>
int main() {
std::string str = "Hello";
str += " World"; // 尾部插入
str.insert(5, ", "); // 在指定位置插入
str.erase(5, 2); // 删除指定范围的字符
std::cout << str << std::endl;
return 0;
}
底层实现是动态数组:内存连续,支持随机访问。
扩容机制:
当 string 的容量不足以容纳新字符时,会分配一块更大的内存(通常是当前容量的两倍)。
将现有字符拷贝到新内存中,然后释放旧内存。
优点:
随机访问效率高(O(1))。
尾部插入和删除效率高(平均 O(1))。
内存连续,缓存友好。
缺点:
插入和删除中间字符效率低(O(n))。
扩容时需要重新分配内存并拷贝数据
(二)关联式容器
1. map(基于红黑树)
std::map 是一个基于红黑树的有序映射,键值对按键的顺序存储。
示例:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};
m[4] = "four"; // 插入键值对
m.insert({5, "five"}); // 插入键值对
for (const auto& pair : m) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
底层实现是红黑树:一种自平衡二叉搜索树,满足以下性质:
①每个节点是红色或黑色。
②根节点是黑色。
③所有叶子节点(空节点)是黑色。
④如果一个节点是红色,则它的两个子节点都是黑色。
⑤从任意节点到其每个叶子的所有路径都包含相同数量的黑色节点。
优点:
插入、删除和查找操作的平均时间复杂度为 O(log n)。
键值对按顺序存储,支持范围查询。
缺点:
内存占用较大(每个节点包含额外的指针)。
插入和删除操作涉及树的重新平衡,复杂度较高。
2. unordered_map(基于哈希表)
std::unordered_map 是一个基于哈希表的无序映射,键值对按哈希值存储。
示例:
#include <iostream>
#include <unordered_map>
int main() {
std::unordered_map<int, std::string> um = {{1, "one"}, {2, "two"}, {3, "three"}};
um[4] = "four"; // 插入键值对
um.insert({5, "five"}); // 插入键值对
for (const auto& pair : um) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
底层实现是哈希表:通过哈希函数将键映射到桶中,支持链表或开放寻址解决冲突。哈希函数将键映射到桶索引。如果发生冲突,使用链表或开放寻址解决冲突。
优点:
插入、删除和查找操作的平均时间复杂度为 O(1)。
内存占用相对较小。
缺点:
最坏情况下(大量冲突)时间复杂度为 O(n)。
不保证键值对的顺序。
3. set(基于红黑树)
std::set 是一个基于红黑树的有序集合,存储唯一键值。
示例:
#include <iostream>
#include <set>
int main() {
std::set<int> s = {1, 2, 3, 4, 5};
s.insert(6); // 插入元素
s.erase(3); // 删除元素
for (int x : s) {
std::cout << x << " ";
}
return 0;
}
底层实现是红黑树:一种自平衡二叉搜索树,满足以下性质:
①每个节点是红色或黑色。
②根节点是黑色。
③所有叶子节点(空节点)是黑色。
④如果一个节点是红色,则它的两个子节点都是黑色。
⑤从任意节点到其每个叶子的所有路径都包含相同数量的黑色节点。
优点:
插入、删除和查找操作的平均时间复杂度为 O(log n)。
键值按顺序存储,支持范围查询。
缺点:
内存占用较大(每个节点包含额外的指针)。
插入和删除操作涉及树的重新平衡,复杂度较高。
4. unordered_set(基于哈希表)
std::unordered_set 是一个基于哈希表的无序集合,存储唯一键值。
示例:
#include <iostream>
#include <unordered_set>
int main() {
std::unordered_set<int> us = {1, 2, 3, 4, 5};
us.insert(6); // 插入元素
us.erase(3); // 删除元素
for (int x : us) {
std::cout << x << " ";
}
return 0;
}
底层实现是哈希表:通过哈希函数将键映射到桶中,支持链表或开放寻址解决冲突。哈希函数将键映射到桶索引。如果发生冲突,使用链表或开放寻址解决冲突。
优点:
插入、删除和查找操作的平均时间复杂度为 O(1)。
内存占用相对较小。
缺点:
最坏情况下(大量冲突)时间复杂度为 O(n)。
不保证键值的顺序。
5. bitset(位图)
std::bitset 是一个固定大小的位集合,支持位操作。
示例:
#include <iostream>
#include <bitset>
int main() {
std::bitset<8> bs(0b10101010); // 初始化为 8 位,值为 10101010
bs.set(1); // 设置第 1 位为 1
bs.reset(2); // 将第 2 位设置为 0
bs.flip(3); // 翻转第 3 位
std::cout << bs << std::endl; // 输出位集合
return 0;
}
底层实现是固定大小的位集合:内存连续,支持位操作。
操作:
set(pos):将第 pos 位设置为 1。
reset(pos):将第 pos 位设置为 0。
flip(pos):翻转第 pos 位。
test(pos):检查第 pos 位是否为 1。
优点:
位操作效率高(O(1))。
内存占用小,适合存储大量布尔值。
缺点:
大小固定,无法动态扩展。
不支持范围操作。
(三)容器适配器
1. stack(栈)
std::stack 是一个后进先出(LIFO)的栈,支持在栈顶插入和删除元素。
示例:
#include <iostream>
#include <stack>
int main() {
std::stack<int> stk;
stk.push(1); // 入栈
stk.push(2);
stk.push(3);
while (!stk.empty()) {
std::cout << stk.top() << " "; // 访问栈顶元素
stk.pop(); // 出栈
}
return 0;
}
默认底层容器:std::deque。
常见操作:
push(value):在栈顶插入元素。
pop():删除栈顶元素。
top():访问栈顶元素。
empty():检查栈是否为空。
size():获取栈的大小。
优点:
简单易用,适合需要后进先出操作的场景。
支持高效的插入和删除操作(O(1))。
缺点:
只能访问栈顶元素,不支持随机访问。
无法直接访问中间元素。
2. queue(队列)
std::queue 是一个先进先出(FIFO)的队列,支持在队尾插入元素,在队首删除元素。
示例:
#include <iostream>
#include <queue>
int main() {
std::queue<int> q;
q.push(1); // 入队
q.push(2);
q.push(3);
while (!q.empty()) {
std::cout << q.front() << " "; // 访问队首元素
q.pop(); // 出队
}
return 0;
}
默认底层容器:std::deque。
常见操作:
push(value):在队尾插入元素。
pop():删除队首元素。
front():访问队首元素。
back():访问队尾元素。
empty():检查队列是否为空。
size():获取队列的大小。
优点:
简单易用,适合需要先进先出操作的场景。
支持高效的插入和删除操作(O(1))。
缺点:
只能访问队首和队尾元素,不支持随机访问。
无法直接访问中间元素。
3. priority_queue(优先队列)
std::priority_queue 是一个优先队列,支持高效的插入和删除操作,元素按优先级顺序排列。
示例:
#include <iostream>
#include <queue>
int main() {
std::priority_queue<int> pq;
pq.push(1); // 插入元素
pq.push(3);
pq.push(2);
while (!pq.empty()) {
std::cout << pq.top() << " "; // 访问优先级最高的元素
pq.pop(); // 删除优先级最高的元素
}
return 0;
}
默认底层容器:std::vector。
操作:
push(value):插入元素,自动按优先级排序。
pop():删除优先级最高的元素。
top():访问优先级最高的元素。
empty():检查优先队列是否为空。
size():获取优先队列的大小。
优点:
支持高效的插入和删除操作(O(log n))。
元素自动按优先级排序,适合需要按优先级处理元素的场景。
缺点:
只能访问优先级最高的元素,不支持随机访问。
无法直接访问中间元素。
(四)常见的算法
1. 非修改序列算法
这些算法用于检查或操作序列中的元素,但不会修改元素本身。
-
std::find:查找指定值的元素,返回找到的第一个元素的迭代器。 -
std::count:统计指定值的元素数量。 -
std::equal:检查两个范围是否相等。 -
std::all_of:检查范围内所有元素是否满足指定条件。 -
std::any_of:检查范围内是否有任意元素满足指定条件。 -
std::none_of:检查范围内是否有任意元素不满足指定条件。 -
std::for_each:对范围内的每个元素执行指定操作。
2. 修改序列算法
这些算法用于修改序列中的元素。
-
std::copy:将一个范围的元素拷贝到另一个范围。 -
std::move:将一个范围的元素移动到另一个范围。 -
std::fill:将指定值填充到一个范围。 -
std::transform:对范围内的每个元素应用函数,并将结果存储到另一个范围。 -
std::reverse:反转范围内的元素顺序。 -
std::sort:对范围内的元素进行排序。 -
std::unique:移除范围内的重复元素。
3. 非修改关联算法
这些算法用于处理关联容器(如 std::map 和 std::set)。
-
std::find_if:查找满足指定条件的第一个元素。 -
std::find_if_not:查找不满足指定条件的第一个元素。 -
std::count_if:统计满足指定条件的元素数量。 -
std::mismatch:查找两个范围中第一个不匹配的元素对。 -
std::equal:检查两个范围是否相等(支持自定义比较)。
4. 修改关联算法
这些算法用于修改关联容器中的元素。
-
std::remove:移除范围内的指定值的元素。 -
std::remove_if:移除满足指定条件的元素。 -
std::replace:将范围内的指定值的元素替换为另一个值。 -
std::replace_if:将满足指定条件的元素替换为另一个值。 -
std::erase:移除范围内的元素(需要容器支持)。
5. 排序算法
这些算法用于对序列进行排序和查找操作。
-
std::sort:对范围内的元素进行排序。 -
std::stable_sort:对范围内的元素进行稳定排序。 -
std::partial_sort:对范围内的部分元素进行排序。 -
std::nth_element:将范围内的第 n 个元素放到正确的位置。 -
std::lower_bound:查找范围内的第一个不小于指定值的元素。 -
std::upper_bound:查找范围内的第一个大于指定值的元素。 -
std::equal_range:查找范围内的指定值的上下界。
6. 数值算法
这些算法用于对序列进行数值计算。
-
std::accumulate:计算范围内的元素的累加和。 -
std::inner_product:计算两个范围内的元素的内积。 -
std::partial_sum:计算范围内的部分和。 -
std::adjacent_difference:计算范围内的相邻元素的差。
7. 其他算法
这些算法用于其他用途。
-
std::for_each:对范围内的每个元素执行指定操作。 -
std::generate:用生成器函数填充范围内的元素。 -
std::shuffle:随机打乱范围内的元素顺序。 -
std::next_permutation:生成范围内的下一个排列。 -
std::prev_permutation:生成范围内的上一个排列。
二、STL 的六大核心组件
(一)容器
容器是用于存储和管理数据的对象。STL 提供了多种类型的容器,每种容器都有其特定的用途和性能特点。
序列容器:
std::vector:动态数组,支持随机访问。
std::list:双向链表,支持任意位置的高效插入和删除。
std::deque:双端队列,支持两端的高效插入和删除。
std::array(C++11):固定大小的数组,支持随机访问。
std::forward_list(C++11):单向链表,内存占用小。
关联容器:
std::set 和 std::multiset:基于红黑树的有序集合。
std::map 和 std::multimap:基于红黑树的有序映射。
std::unordered_set 和 std::unordered_multiset:基于哈希表的无序集合。
std::unordered_map 和 std::unordered_multimap:基于哈希表的无序映射。
容器适配器:
std::stack:后进先出(LIFO)的栈。
std::queue:先进先出(FIFO)的队列。
std::priority_queue:优先队列,支持高效的插入和删除操作。
(二)算法
算法是用于对容器中的元素进行操作的函数模板。STL 提供了一组通用算法,这些算法通过迭代器与容器进行交互。
非修改序列算法:如 std::find、std::count、std::equal。
修改序列算法:如 std::copy、std::transform、std::sort。
排序算法:如 std::sort、std::stable_sort、std::nth_element。
数值算法:如 std::accumulate、std::inner_product。
(三)迭代器
迭代器是用于访问容器中元素的对象,类似于指针。迭代器是容器和算法之间的纽带。
Input Iterator:只读迭代器,支持单向遍历。
Output Iterator:只写迭代器,支持单向遍历。
Forward Iterator:支持单向遍历,可以多次遍历。
Bidirectional Iterator:支持双向遍历。
Random Access Iterator:支持随机访问,可以进行任意位置的访问。
Contiguous Iterator(C++20):支持连续存储的迭代器。
(四)适配器
适配器是通过封装和扩展其他组件的功能,将一个容器或迭代器转换为另一种数据结构或接口。
容器适配器:
std::stack:后进先出(LIFO)的栈。
std::queue:先进先出(FIFO)的队列。
std::priority_queue:优先队列,支持高效的插入和删除操作。
迭代器适配器:
std::reverse_iterator:反向迭代器。
std::insert_iterator:插入迭代器。
std::ostream_iterator:输出流迭代器。
(五)仿函数
仿函数是结合函数和对象的特性,重载 operator(),用于算法的行为定制。
标准仿函数:
std::plus、std::minus、std::multiplies、std::divides。
std::equal_to、std::not_equal_to、std::less、std::greater。
自定义仿函数:
通过重载 operator() 实现自定义行为。
(六)空间配置器
空间配置器为容器提供内存管理支持。容器通过分配器动态分配和释放内存,从而存储和管理数据。
默认分配器:std::allocator。
自定义分配器:用户可以自定义分配器,以满足特定的内存管理需求。
三、面试常见问题
1. vector和list的区别?
vector 是基于动态数组实现的,支持高效的随机访问和尾部插入删除操作,但插入删除中间元素效率较低,扩容时需要重新分配内存并拷贝数据。
list 是基于双向链表实现的,支持在任意位置高效地插入和删除元素,但不支持随机访问,内存占用较大。
如果需要频繁的随机访问和尾部操作,vector 是更好的选择;如果需要在任意位置频繁插入和删除元素,list 更合适。
2. vector是如何插入数据的?
插入数据:
尾部插入:通过 push_back 方法在 vector 的尾部添加一个元素。如果当前容量足够,直接在尾部添加元素;如果容量不足,会触发扩容操作。
中间插入:通过 insert 方法在指定位置插入一个元素。这需要将插入点之后的所有元素向后移动一个位置,因此时间复杂度为 O(n)。
扩容机制:
容量检查:当 vector 的 size() 达到当前 capacity() 时,需要扩容。
分配新内存:通常会分配一块更大的内存,通常是当前容量的两倍(具体倍数由实现决定)。
拷贝数据:将现有元素从旧内存拷贝到新内存。
释放旧内存:释放旧内存,更新指针指向新内存。
3. vector和list都有缺点,有什么其他的设计方案, 能解决问题?
vector 插入和删除操作在非尾部位置时效率较低,而 list 是双向链表随机访问速度慢,需要逐个遍历节点。:中间方案是deque,它结合了vector和list的优点,支持两端操作且随机访问,但是效率不高。
4. map和set底层是什么?
map 和 set 的底层实现通常是红黑树。红黑树是一种自平衡的二叉查找树,通过以下规则保持平衡:
①每个节点是红色或黑色。
②根节点是黑色。
③所有叶子节点(空节点)是黑色。
④如果一个节点是红色,则它的两个子节点都是黑色。
⑤从任意节点到其每个叶子的所有路径都包含相同数量的黑色节点。
查找效率:时间复杂度为 O(logn),因为树的高度被限制在 O(logn)。
插入和删除效率:时间复杂度也是 O(logn),因为需要调整树的结构以保持平衡。
大致实现:
插入:先按照二叉查找树的方式插入节点,然后通过旋转和颜色调整来恢复红黑树的性质。
删除:先删除节点,再通过旋转和颜色调整来保持平衡。
查找:从根节点开始,根据键值大小比较,向左或向右遍历,直到找到目标节点或到达叶子节点。
5. map的operator[ ]实现是什么?
map 的 operator[] 是一个非常重要的成员函数,用于访问或插入键值对。
访问已存在的键:如果键已经存在于 map 中,operator[] 返回对应键的值的引用。插入新键:如果键不存在于 map 中,operator[] 会自动插入一个键值对,其中键是提供的键,值是值类型的默认值(例如,对于 int 类型,默认值是 0),并返回这个新值的引用。
operator[] 的实现可以分为以下几个步骤:查找键是否存在:通过红黑树的查找操作,确定键是否已经存在于 map 中。-------> 键存在时:直接返回对应键的值的引用。-------> 键不存在时:插入一个新键值对,值为默认值,并返回这个新值的引用。
6. 一个类型做map和set的key有什么要求?
能支持比较大小或者提供仿函数比较大小
7. map和set和multi_xxx的区别?
map和set的元素是唯一的。multimap和multiset的元素可以重复
map 和 set默认使用红黑树(自平衡二叉查找树)实现。元素按键(或值)的顺序存储,提供对元素的有序访问。multimap和multiset同样默认使用红黑树实现。元素按键(或值)的顺序存储。但允许键(或值)重复。
map和set:插入操作会检查键(或值)是否已存在。如果键(或值)已存在,插入操作会失败。查找操作返回指向找到的元素的迭代器。multimap和multiset插入操作不会检查键(或值)是否已存在。允许插入多个相同的键(或值)。查找操作返回一个范围(迭代器对),包含所有匹配的元素。
8. unordered_map和unordered_set是如何实现的?
unordered_map和unordered_set 是基于哈希表实现的关联容器。它们的实现依赖于哈希函数和冲突解决机制。
哈希表:通过哈希函数将键映射到桶,使用链地址法或开放寻址法解决冲突。
效率:平均情况下查找、插入和删除操作的时间复杂度为 O(1),但在最坏情况下可能退化到 O(n)。
实现:通过动态数组存储桶,每个桶是一个链表(或平衡树),并根据负载因子动态调整桶的数量。
9. unordered_map和map的区别是什么?
unordered_map底层是哈希表和map底层是红黑树
哈希表:
基于哈希函数将键映射到一个较小范围的整数(通常是数组的索引)。使用链地址法或开放寻址法解决冲突。内部通常是一个动态数组,每个桶可以是一个链表或平衡树。
红黑树:
是一种自平衡二叉查找树。每个节点存储一个键值对,并通过比较键的大小来维护树的有序性。遵循红黑树的五条规则,通过旋转和颜色调整来保持平衡。
10. 一个类型做unordered_map和unordered_set有什 么要求?
作为 unordered_map 或 unordered_set 的键,类型必须满足可哈希性(提供哈希函数)和可比较性(支持等价性比较)。
11. C++的优缺点
设计复杂,更新慢,菱形继承
12. 容器是否是线程安全的?
不是,没有加锁
13. 迭代器失效是什么?
指STL中容器的增删操改变了其内部结构,导致 迭代器失效。vector由于经过插入操作以后要进行扩容,将原元素复制到新空间中,原空间的迭代器失效,让迭代器自动更新就行。
14. STL的优缺点是什么?
STL优点:高效、通用、安全且灵活,提供丰富的容器和算法,减少重复代码,提升开发效率。STL缺点:学习曲线陡,模板错误信息复杂,性能和内存占用在某些场景下可能不如手写代码优化,编译时间可能较长。
4万+

被折叠的 条评论
为什么被折叠?



