C++ 标准模板库(STL)中的容器是 C++ 编程的基石。掌握它们是高效编程的关键。
下面我将详细介绍几个最重要和最常用的容器,并将它们分为三类:序列容器 、关联容器 和容器适配器。
一、序列容器 (Sequence Containers)
这类容器中的元素是线性排列的,就像一个队伍。你可以通过元素在队伍中的位置(索引)来访问它们。
1. std::vector
:动态数组(最重要的容器)
-
一句话总结 :C++ 的"瑞士军刀",当你不知道用什么容器时,就用
vector
。 -
核心特性:
- 底层结构:一块连续的内存,和普通数组一样。
- 访问 :极快的随机访问速度。通过索引
vec[i]
访问元素是 O(1) 操作。 - 尾部操作 :在末尾添加 (
push_back
) 或删除 (pop_back
) 元素非常快(均摊 O(1))。 - 中间操作:在中间或开头插入/删除元素很慢(O(N)),因为它需要移动之后的所有元素。
-
适用场景:
- 需要频繁地随机访问元素。
- 主要在容器末尾进行添加/删除操作。
- 作为函数参数或返回值传递一组数据。
-
代码示例:
cpp#include <iostream> #include <vector> #include <string> int main() { // 创建一个存储字符串的 vector std::vector<std::string> names; // 1. 添加元素 (在尾部) names.push_back("Alice"); names.push_back("Bob"); names.push_back("Charlie"); // 2. 随机访问元素 (O(1)) std::cout << "The first name is: " << names[0] << std::endl; names[1] = "Robert"; // 修改元素 // 3. 遍历容器 std::cout << "\nAll names:" << std::endl; for (const std::string& name : names) { std::cout << "- " << name << std::endl; } // 4. 在中间插入 (O(N),较慢) names.insert(names.begin() + 1, "Eve"); // 在索引 1 处插入 // 5. 删除元素 names.pop_back(); // 删除最后一个元素 "Charlie" std::cout << "\nAfter modifications:" << std::endl; for (const auto& name : names) { std::cout << "- " << name << std::endl; } std::cout << "Current size: " << names.size() << std::endl; }
2. std::list
:双向链表
-
一句话总结:为频繁的插入和删除操作而生。
-
核心特性:
- 底层结构:非连续的双向链表,每个元素都指向前一个和后一个元素。
- 访问 :不支持快速随机访问。要访问第
i
个元素,必须从头或尾开始遍历i
步(O(N))。 - 任意位置操作:在任何位置插入或删除元素都非常快(O(1)),只要你已经有了指向该位置的迭代器。
- 迭代器稳定性:插入或删除元素不会使指向其他元素的迭代器失效。
-
适用场景:
- 需要对容器进行大量的插入和删除操作,尤其是在中间位置。
- 不需要随机访问。
-
代码示例:
cpp#include <iostream> #include <list> int main() { std::list<int> numbers; numbers.push_back(10); numbers.push_back(20); numbers.push_front(5); // [5, 10, 20] auto it = numbers.begin(); it++; // it 指向 10 // 在 10 的前面插入 8 (O(1)) numbers.insert(it, 8); // [5, 8, 10, 20] // 删除 10 (O(1)) numbers.erase(it); // [5, 8, 20] std::cout << "List content:" << std::endl; for (int n : numbers) { std::cout << n << " "; } std::cout << std::endl; }
二、关联容器 (Associative Containers)
这类容器自动对其元素进行排序 ,并允许通过键 (Key) 来快速查找。
3. std::map
:有序键值对
-
一句话总结:按键排序的字典。
-
核心特性:
- 底层结构 :通常是红黑树(一种自平衡二叉搜索树)。
- 元素 :存储
std::pair<const Key, Value>
形式的键值对。 - 排序 :所有元素都根据键
Key
自动排序。 - 查找:基于键的查找、插入和删除速度都很快(O(log N))。
-
适用场景:
- 需要存储键值对。
- 需要数据始终按键排序。
- 需要能够按范围查找(例如,查找所有在 'A' 和 'C' 之间的键)。
-
代码示例:
cpp#include <iostream> #include <map> #include <string> int main() { std::map<std::string, int> ages; // 插入元素 ages["Charlie"] = 30; ages["Alice"] = 25; ages["Bob"] = 35; // 查找和访问 std::cout << "Alice's age: " << ages["Alice"] << std::endl; // 检查键是否存在 if (ages.count("David")) { std::cout << "David's age is " << ages["David"] << std::endl; } else { std::cout << "David is not in the map." << std::endl; } // 遍历 (注意输出是按键排序的:Alice, Bob, Charlie) std::cout << "\nAll ages (sorted by name):" << std::endl; for (const auto& pair : ages) { std::cout << pair.first << " is " << pair.second << " years old." << std::endl; } }
4. std::unordered_map
:无序键值对(非常常用)
-
一句话总结:性能更高的字典,但没有排序。
-
核心特性:
- 底层结构 :哈希表 (Hash Table) 。
- 元素:存储键值对。
- 排序:元素是无序的,遍历顺序不确定。
- 查找:基于键的查找、插入和删除速度极快(平均 O(1))。
-
适用场景:
- 需要存储键值对,且追求极致的查找性能。
- 不需要数据排序。这是绝大多数字典应用场景的首选。
-
代码示例:
cpp#include <iostream> #include <unordered_map> #include <string> int main() { std::unordered_map<std::string, int> ages; ages["Charlie"] = 30; ages["Alice"] = 25; ages["Bob"] = 35; std::cout << "Bob's age: " << ages["Bob"] << std::endl; // 遍历 (输出顺序不确定) std::cout << "\nAll ages (in arbitrary order):" << std::endl; for (const auto& pair : ages) { std::cout << pair.first << " is " << pair.second << " years old." << std::endl; } }
std::set
和 std::unordered_set
set
容器可以看作是只有键没有值的 map
。它们用于存储唯一的、不重复的元素集合。
std::set
:有序集合,基于红黑树,O(log N) 操作。std::unordered_set
:无序集合,基于哈希表,平均 O(1) 操作。
它们非常适合用于去重或快速检查某个元素是否存在。
三、容器适配器 (Container Adapters)
它们不是真正的容器,而是对现有序列容器(如 vector
, deque
, list
)的封装,提供了特定的接口。
5. std::stack
:栈
-
一句话总结:后进先出 (LIFO - Last-In, First-Out) 的数据结构。
-
核心特性 :只能在"顶部"添加 (
push
) 和移除 (pop
) 元素。 -
适用场景:函数调用栈、括号匹配、撤销/重做功能。
-
代码示例:
cpp#include <iostream> #include <stack> // 包含头文件 int main() { std::stack<int> s; s.push(1); // [1] s.push(2); // [1, 2] s.push(3); // [1, 2, 3] std::cout << "Top element: " << s.top() << std::endl; // 输出 3 s.pop(); // 移除 3, 栈变为 [1, 2] std::cout << "Top element after pop: " << s.top() << std::endl; // 输出 2 std::cout << "Is stack empty? " << (s.empty() ? "Yes" : "No") << std::endl; }
6. std::queue
:队列
-
一句话总结:先进先出 (FIFO - First-In, First-Out) 的数据结构。
-
核心特性 :在队尾 (
push
) 添加元素,在队头 (pop
) 移除元素。 -
适用场景:任务队列、广度优先搜索 (BFS)、打印机任务。
-
代码示例:
cpp#include <iostream> #include <queue> // 包含头文件 int main() { std::queue<std::string> q; q.push("Task 1"); q.push("Task 2"); q.push("Task 3"); std::cout << "Front element: " << q.front() << std::endl; // 输出 Task 1 std::cout << "Back element: " << q.back() << std::endl; // 输出 Task 3 q.pop(); // 移除 Task 1 std::cout << "Front element after pop: " << q.front() << std::endl; // 输出 Task 2 }
总结与如何选择
容器 | 底层结构 | 排序性 | 随机访问 | 中间插入/删除 | 主要用途 |
---|---|---|---|---|---|
std::vector |
动态数组 | 按插入顺序 | O(1) | O(N) | 默认首选,通用数据存储 |
std::list |
双向链表 | 按插入顺序 | O(N) | O(1) | 频繁的任意位置插入/删除 |
std::map |
红黑树 | 按键排序 | N/A | O(log N) | 有序的键值对存储 |
std::unordered_map |
哈希表 | 无序 | N/A | O(1) | 高性能的键值对查找 |
std::set |
红黑树 | 按键排序 | N/A | O(log N) | 有序的唯一元素集合 |
std::unordered_set |
哈希表 | 无序 | N/A | O(1) | 高性能的唯一元素查找 |
std::stack |
vector 或deque |
LIFO | N/A | N/A | 后进先出场景 |
std::queue |
deque |
FIFO | N/A | N/A | 先进先出场景 |
简单选择流程:
-
需要键值对吗?
-
是 -> 需要排序吗?
- 是 ->
std::map
- 否 ->
std::unordered_map
(通常是更好的选择)
- 是 ->
-
-
不需要键值对,只需要存储一串元素?
-
需要快速随机访问吗?(用
[]
访问)- 是 ->
std::vector
(绝大多数情况)
- 是 ->
-
需要频繁在中间插入/删除吗?
- 是 ->
std::list
- 是 ->
-
-
需要后进先出 (LIFO) 或先进先出 (FIFO) 的简单接口吗?
- 是 ->
std::stack
或std::queue
- 是 ->