在 C++ 面试中,STL 容器是非常高频的考点,尤其是
vector、map和unordered_map。这三个容器在实际开发中使用很多,面试官通常会考察它们的底层结构、使用场景、时间复杂度以及区别。本文主要从面试角度整理这三个常见容器,并结合代码进行说明。
一、vector:动态数组
vector 可以理解为一个"可以自动扩容的数组"。它的底层是一段连续的内存空间,所以支持下标访问,访问速度很快。
1. vector 的基本使用
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 创建一个 int 类型的 vector
vector<int> nums;
// 向 vector 尾部添加元素
nums.push_back(10);
nums.push_back(20);
nums.push_back(30);
// 使用下标访问元素
cout << nums[0] << endl; // 输出 10
cout << nums[1] << endl; // 输出 20
// 遍历 vector
for (int i = 0; i < nums.size(); i++) {
cout << nums[i] << " ";
}
return 0;
}
2. vector 的特点
vector 的底层是连续内存,因此可以像数组一样通过下标快速访问元素。
cpp
cout << nums[2] << endl;
这种访问方式的时间复杂度是 O(1)。
但是,如果在中间插入或删除元素,效率就比较低,因为后面的元素需要整体移动。
cpp
nums.insert(nums.begin() + 1, 100);
这行代码表示在下标 1 的位置插入 100,后面的元素都要往后移动。
3. vector 的扩容机制
当 vector 当前容量不够时,会重新申请一块更大的连续内存,然后把原来的元素拷贝或移动到新空间中。
例如:
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums;
for (int i = 0; i < 10; i++) {
nums.push_back(i);
// size 表示当前元素个数
// capacity 表示当前已经分配的容量
cout << "size = " << nums.size()
<< ", capacity = " << nums.capacity() << endl;
}
return 0;
}
这里需要注意:
size() 表示已经存了多少个元素。
capacity() 表示当前空间最多能存多少个元素。
当 size() 超过 capacity() 时,vector 会自动扩容。
4. vector 面试总结
面试回答时可以这样说:
vector 底层是动态数组,内存连续,支持随机访问,下标访问时间复杂度是 O(1)。尾部插入效率较高,通常是均摊 O(1)。但是中间插入和删除效率较低,因为需要移动元素。vector 扩容时会重新申请更大的内存空间,并把原来的元素拷贝或移动过去,因此扩容可能导致原来的迭代器、指针、引用失效。
二、map:有序键值对容器
map 用来存储键值对,也就是 key-value 结构。它会根据 key 自动排序。
例如:
cpp
map<string, int> scores;
这里 string 是 key,int 是 value,可以理解为"名字对应分数"。
1. map 的基本使用
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
// 创建一个 map,key 是 string,value 是 int
map<string, int> scores;
// 插入数据
scores["Tom"] = 90;
scores["Jack"] = 85;
scores["Alice"] = 95;
// 根据 key 查找 value
cout << scores["Tom"] << endl; // 输出 90
// 遍历 map
// map 会按照 key 自动排序
for (auto it = scores.begin(); it != scores.end(); it++) {
cout << it->first << " : " << it->second << endl;
}
return 0;
}
在上面的代码中:
cpp
it->first
表示 key。
cpp
it->second
表示 value。
2. map 的底层结构
map 的底层通常是红黑树。
红黑树是一种自平衡二叉搜索树,它可以保证插入、删除、查找的时间复杂度都是 O(log n)。
所以 map 的特点是:
自动按 key 排序
查找效率稳定
插入和删除效率为 O(log n)
3. map 的查找方式
推荐使用 find() 查找元素。
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<string, int> scores;
scores["Tom"] = 90;
scores["Jack"] = 85;
// 使用 find 查找 key 是否存在
auto it = scores.find("Tom");
if (it != scores.end()) {
cout << "找到了,分数是:" << it->second << endl;
} else {
cout << "没有找到" << endl;
}
return 0;
}
注意:
cpp
scores["Tom"]
可以访问元素。
但是如果 key 不存在,使用 [] 会自动插入一个默认值。
例如:
cpp
cout << scores["Bob"] << endl;
如果 Bob 不存在,map 会自动创建一个 "Bob",并给它一个默认值 0。
所以在只判断 key 是否存在时,更推荐使用 find()。
4. map 面试总结
面试回答时可以这样说:
map 是一个有序的键值对容器,底层通常是红黑树。它会根据 key 自动排序,查找、插入和删除的时间复杂度都是 O(log n)。如果需要有序存储,或者需要按照 key 的大小顺序遍历,就可以使用 map。
三、unordered_map:无序键值对容器
unordered_map 也是存储键值对的容器,但是它不会根据 key 排序。
它的底层通常是哈希表,所以查找速度很快。
1. unordered_map 的基本使用
cpp
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;
int main() {
// 创建一个 unordered_map,key 是 string,value 是 int
unordered_map<string, int> scores;
// 插入数据
scores["Tom"] = 90;
scores["Jack"] = 85;
scores["Alice"] = 95;
// 根据 key 访问 value
cout << scores["Tom"] << endl;
// 遍历 unordered_map
// 注意:unordered_map 不保证遍历顺序
for (auto it = scores.begin(); it != scores.end(); it++) {
cout << it->first << " : " << it->second << endl;
}
return 0;
}
unordered_map 的遍历结果不一定按照插入顺序,也不一定按照 key 的大小顺序。
2. unordered_map 的底层结构
unordered_map 的底层通常是哈希表。
它会根据 key 计算哈希值,然后把数据放到对应的位置中。
正常情况下,查找、插入、删除的平均时间复杂度是 O(1)。
但是如果发生大量哈希冲突,最坏情况下可能退化到 O(n)。
3. unordered_map 常见使用场景
在刷题和实际开发中,unordered_map 很常用于快速查找。
例如经典的 Two Sum 问题:
cpp
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
vector<int> twoSum(vector<int>& nums, int target) {
// key 存数组元素的值,value 存数组元素的下标
unordered_map<int, int> mp;
for (int i = 0; i < nums.size(); i++) {
// 当前数字是 nums[i]
// 需要找的另一个数字是 target - nums[i]
int need = target - nums[i];
// 如果 need 已经在哈希表中,说明找到了答案
if (mp.find(need) != mp.end()) {
return {mp[need], i};
}
// 如果没有找到,就把当前数字和下标存入哈希表
mp[nums[i]] = i;
}
// 没有找到结果,返回空数组
return {};
}
int main() {
vector<int> nums = {2, 7, 11, 15};
int target = 9;
vector<int> result = twoSum(nums, target);
if (!result.empty()) {
cout << result[0] << " " << result[1] << endl;
}
return 0;
}
这段代码中,unordered_map 的作用就是快速判断某个数字是否已经出现过。
如果使用双重循环,时间复杂度是 O(n^2)。
如果使用 unordered_map,平均时间复杂度可以降到 O(n)。
4. unordered_map 面试总结
面试回答时可以这样说:
unordered_map 是无序键值对容器,底层通常是哈希表。它不保证元素有序,但是查找效率很高,平均时间复杂度是 O(1)。如果只关心快速查找,不关心顺序,通常可以使用 unordered_map。
四、vector、map、unordered_map 对比
| 容器 | 底层结构 | 是否有序 | 查找效率 | 适合场景 |
|---|---|---|---|---|
| vector | 动态数组 | 按插入顺序存储 | 按下标访问 O(1) | 需要连续存储、频繁随机访问 |
| map | 红黑树 | 按 key 自动排序 | O(log n) | 需要 key 有序 |
| unordered_map | 哈希表 | 无序 | 平均 O(1) | 需要快速查找 |
简单来说:
需要下标访问,用 vector。
需要 key 自动排序,用 map。
需要快速查找,不关心顺序,用 unordered_map。
五、面试高频问题整理
1. vector 和普通数组有什么区别?
普通数组的大小是固定的,一旦定义之后,数组的长度就不能改变。例如:
cpp
int arr[5] = {1, 2, 3, 4, 5};
这个数组最多只能存 5 个整数,不能自动变大。
而 vector 是动态数组,可以根据元素数量自动扩容。例如:
cpp
vector<int> nums;
nums.push_back(10);
nums.push_back(20);
nums.push_back(30);
vector 的使用更加灵活,不需要一开始就确定最终要存多少个元素。
面试时可以这样回答:
vector 和数组的底层都是连续内存,因此都支持下标访问。但是数组大小固定,不能自动扩容;vector 是动态数组,可以自动扩容,使用起来更灵活。vector 还提供了 push_back()、insert()、erase() 等常用接口,开发效率更高。
2. vector 的底层原理是什么?
vector 的底层是一段连续的内存空间。它一般会维护三个重要信息:
起始位置
当前元素个数 size
当前容量 capacity
其中:
cpp
nums.size(); // 当前已经存了多少个元素
nums.capacity(); // 当前最多能存多少个元素
示例代码:
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums;
for (int i = 0; i < 10; i++) {
nums.push_back(i);
cout << "当前元素个数 size = " << nums.size()
<< ",当前容量 capacity = " << nums.capacity() << endl;
}
return 0;
}
当 size 超过 capacity 时,vector 会重新申请一块更大的内存空间,然后把原来的元素拷贝或移动到新空间中。
面试时可以这样回答:
vector 底层是动态数组,内存连续,所以支持随机访问,下标访问时间复杂度是 O(1)。当容量不够时,vector 会重新申请更大的空间,并把原来的元素移动过去,这就是 vector 的扩容机制。
3. vector 为什么扩容会导致迭代器失效?
因为 vector 的底层是连续内存。如果当前容量不够,vector 会重新申请一块新的内存,然后把原来的元素移动到新的内存中。
这时,原来的迭代器、指针、引用还指向旧的内存地址,而旧内存可能已经被释放,所以它们就失效了。
例如:
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums;
nums.push_back(1);
nums.push_back(2);
// 保存第一个元素的地址
int* p = &nums[0];
// 继续添加元素,可能触发 vector 扩容
nums.push_back(3);
nums.push_back(4);
nums.push_back(5);
// 如果发生扩容,p 可能已经失效
// 此时继续使用 p 就是不安全的
cout << nums[0] << endl;
return 0;
}
面试时可以这样回答:
因为 vector 扩容时会重新申请内存,原来元素的地址可能发生变化,所以原来的迭代器、指针和引用都会失效。为了避免这个问题,可以提前使用 reserve() 预留空间,减少扩容次数。
例如:
vector<int> nums;
// 提前预留 100 个元素的空间
nums.reserve(100);
4. vector 的 push_back 和 emplace_back 有什么区别?
push_back() 是把一个已经创建好的对象放入 vector 中。
emplace_back() 是直接在 vector 的尾部构造对象,可以减少一次临时对象的创建或拷贝。
示例代码:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Student {
public:
string name;
int age;
Student(string n, int a) {
name = n;
age = a;
cout << "构造 Student 对象" << endl;
}
};
int main() {
vector<Student> students;
// 先创建一个临时对象,再放入 vector
students.push_back(Student("Tom", 18));
// 直接在 vector 内部构造对象
students.emplace_back("Jack", 20);
return 0;
}
面试时可以这样回答:
push_back() 是把对象插入到 vector 尾部,可能涉及临时对象的创建和移动;emplace_back() 是在容器内部直接构造对象,通常效率更高。对于基本类型区别不大,对于复杂对象,emplace_back() 更有优势。
5. map 和 unordered_map 有什么区别?
这是 STL 面试中非常高频的问题。
map 的底层通常是红黑树,元素会按照 key 自动排序。
unordered_map 的底层通常是哈希表,元素不会自动排序。
示例代码:
#include <iostream>
#include <map>
#include <unordered_map>
using namespace std;
int main() {
map<int, string> m;
unordered_map<int, string> um;
m[3] = "three";
m[1] = "one";
m[2] = "two";
um[3] = "three";
um[1] = "one";
um[2] = "two";
cout << "map 遍历结果:" << endl;
for (auto item : m) {
cout << item.first << " : " << item.second << endl;
}
cout << "unordered_map 遍历结果:" << endl;
for (auto item : um) {
cout << item.first << " : " << item.second << endl;
}
return 0;
}
map 遍历时会按照 key 的大小顺序输出,而 unordered_map 的遍历顺序是不固定的。
面试时可以这样回答:
map 底层是红黑树,key 会自动排序,查找、插入、删除的时间复杂度都是 O(log n)。unordered_map 底层是哈希表,不保证顺序,平均查找、插入、删除时间复杂度是 O(1),但如果哈希冲突严重,最坏可能退化到 O(n)。
6. 什么时候用 map,什么时候用 unordered_map?
如果需要按照 key 的顺序遍历,就使用 map。
例如需要按照学生学号从小到大输出:
map<int, string> students;
students[1003] = "Tom";
students[1001] = "Jack";
students[1002] = "Alice";
for (auto item : students) {
cout << item.first << " : " << item.second << endl;
}
输出时会按照 key 排序。
如果只是为了快速查找,不关心顺序,就使用 unordered_map。
例如统计每个数字出现的次数:
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
int main() {
vector<int> nums = {1, 2, 2, 3, 3, 3};
// key 表示数字,value 表示出现次数
unordered_map<int, int> countMap;
for (int num : nums) {
countMap[num]++;
}
for (auto item : countMap) {
cout << item.first << " 出现了 " << item.second << " 次" << endl;
}
return 0;
}
面试时可以这样回答:
需要有序存储或者按照 key 的顺序遍历时,用 map。如果只关心查找效率,不关心顺序,一般优先使用 unordered_map。
7. 为什么 unordered_map 平均查找是 O(1),最坏是 O(n)?
unordered_map 底层是哈希表。正常情况下,它可以通过 key 直接计算出一个位置,然后快速找到元素,所以平均时间复杂度是 O(1)。
但是如果多个 key 计算出的哈希位置相同,就会发生哈希冲突。冲突太多时,查找就需要在同一个位置上逐个比较,最坏情况下可能退化成 O(n)。
面试时可以这样回答:
unordered_map 通过哈希函数把 key 映射到对应位置,所以平均查找效率是 O(1)。但是如果哈希冲突严重,很多元素落到同一个桶里,查找时就需要逐个比较,所以最坏情况下会退化到 O(n)。
8. map 中使用 \[\] 和 find 有什么区别?
[] 可以访问 key 对应的 value。
但是需要注意:如果 key 不存在,[] 会自动插入一个默认值。
示例代码:
#include <iostream>
#include <map>
using namespace std;
int main() {
map<string, int> scores;
scores["Tom"] = 90;
// Bob 原本不存在
// 使用 [] 后,Bob 会被自动插入,默认值是 0
cout << scores["Bob"] << endl;
cout << "map 的大小是:" << scores.size() << endl;
return 0;
}
所以,如果只是判断一个 key 是否存在,更推荐使用 find()。
auto it = scores.find("Tom");
if (it != scores.end()) {
cout << "找到了:" << it->second << endl;
} else {
cout << "没有找到" << endl;
}
面试时可以这样回答:
[] 不仅可以访问元素,还可能插入新元素。如果 key 不存在,map[key] 会自动创建一个默认值。find() 只负责查找,不会插入新元素,所以判断 key 是否存在时更推荐使用 find()。
9. vector、map、unordered_map 在项目中怎么选?
可以按照使用场景来选择。
如果只是存一组数据,并且经常按照下标访问,就用 vector。
例如存储日志记录、学生列表、文件列表:
vector<string> logs;
logs.push_back("程序启动");
logs.push_back("连接服务器成功");
logs.push_back("接收到数据");
如果需要根据 key 查找 value,并且还要求 key 有序,就用 map。
例如按照编号保存数据,并且希望遍历时自动按编号排序:
map<int, string> idToName;
idToName[1002] = "Tom";
idToName[1001] = "Jack";
如果需要根据 key 快速查找 value,而且不关心顺序,就用 unordered_map。
例如保存用户名和登录次数:
unordered_map<string, int> loginCount;
loginCount["Tom"]++;
loginCount["Jack"]++;
面试时可以这样回答:
如果需要连续存储和下标访问,选择 vector;如果需要 key 有序,选择 map;如果只需要快速查找,不关心顺序,选择 unordered_map。
六、总结
vector、map 和 unordered_map 是 C++ STL 中非常常用的三个容器,也是面试中的高频考点。
vector 可以理解为动态数组。它的底层是连续内存,所以支持下标访问,访问效率很高,时间复杂度是 O(1)。它适合存储一组连续数据,例如数组、列表、日志记录、文件路径等。vector 的优点是访问快、使用方便、尾部插入效率较高;缺点是中间插入和删除效率较低,并且扩容时可能导致迭代器、指针和引用失效。
map 是有序键值对容器,底层通常是红黑树。它会根据 key 自动排序,查找、插入、删除的时间复杂度都是 O(log n)。如果项目中需要按照 key 的大小顺序保存或遍历数据,就可以使用 map。例如按照编号、时间、等级等顺序管理数据。
unordered_map 是无序键值对容器,底层通常是哈希表。它不保证元素顺序,但是平均查找效率很高,查找、插入、删除的平均时间复杂度都是 O(1)。如果只需要根据 key 快速查找 value,不关心顺序,通常可以优先考虑 unordered_map。例如刷题中的快速查找、统计次数、缓存映射等场景。
三者可以这样记忆:
vector:动态数组,内存连续,适合下标访问。
map:红黑树,有序,适合按 key 排序。
unordered_map:哈希表,无序,适合快速查找。
面试中回答 STL 容器问题时,不要只说"会用",最好从下面几个角度回答:
1. 这个容器底层是什么?
2. 是否有序?
3. 查找、插入、删除的时间复杂度是多少?
4. 适合什么使用场景?
5. 有没有需要注意的问题?
例如回答 map 和 unordered_map 的区别时,可以这样组织语言:
map 和 unordered_map 都是键值对容器。map 底层通常是红黑树,会按照 key 自动排序,查找、插入和删除的时间复杂度是 O(log n)。unordered_map 底层通常是哈希表,不保证顺序,平均查找、插入和删除时间复杂度是 O(1),但是哈希冲突严重时可能退化到 O(n)。如果需要有序遍历,就用 map;如果只关心快速查找,不关心顺序,就用 unordered_map。
最后可以简单总结:
需要存一组连续数据:用 vector。
需要 key-value,并且 key 有序:用 map。
需要 key-value,并且快速查找:用 unordered_map。
掌握这三个容器后,基本可以应对很多 C++ STL 相关的基础面试问题。