【C++ 面试高频:STL 容器 vector、map、unordered_map 总结】

在 C++ 面试中,STL 容器是非常高频的考点,尤其是 vectormapunordered_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


六、总结

vectormapunordered_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. 有没有需要注意的问题?

例如回答 mapunordered_map 的区别时,可以这样组织语言:

mapunordered_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 相关的基础面试问题。

0voice · GitHub

相关推荐
我是一颗柠檬1 小时前
【Java项目技术亮点】滑动窗口限流算法
java·开发语言·算法
Irissgwe1 小时前
二叉树进阶
数据结构·c++·算法·c·二叉搜索树
hairenwangmiao1 小时前
c++排序(第一章----桶排序与sort排序)
数据结构·c++·排序
于指尖飞舞1 小时前
java后端面试题(jvm极简)
java·开发语言·jvm
郝学胜-神的一滴1 小时前
[简化版 GAMES 101] 计算机图形学 13:从光栅化到着色——赋予三维像素光影灵魂
c++·计算机视觉·unity·godot·图形渲染·opengl·unreal
java_cj1 小时前
从kubectl源码学Cobra:打造专业级Go命令行工具的完整实践
运维·开发语言·后端·云原生·golang·kubernetes·k8s
AZaLEan__1 小时前
JavaScript 基础语法
开发语言·javascript·ecmascript
影视飓风TIM2 小时前
C++ 核心语法笔记:拷贝构造、深浅拷贝与运算符重载
java·开发语言·javascript
jieyucx2 小时前
Go MongoDB 实战完全指南|从连接、CRUD、BSON结构体映射到高并发避坑全解
开发语言·mongodb·golang