这篇文章我们来全面地梳理一下 C++ 中所有主要的数据结构。我将按照它们的分类进行介绍,并对每一个结构提供其简介、底层实现、时间复杂度、核心使用场景以及一个简明的 C++ 代码示例。
在 C++ 中,绝大多数数据结构都是通过标准模板库 (Standard Template Library, STL) 提供的。熟练使用 STL 是 C++ 开发的关键。
数据结构分类
C++ STL 中的数据结构主要可以分为以下几类:
-
顺序容器 (Sequence Containers):元素按线性顺序排列。
std::vector
std::array
std::list
std::deque
std::forward_list
-
关联容器 (Associative Containers):根据键值自动排序,查找速度快。
std::map
std::set
std::multimap
std::multiset
-
无序关联容器 (Unordered Associative Containers):使用哈希表实现,提供平均 O(1) 的查找速度。
std::unordered_map
std::unordered_set
std::unordered_multimap
std::unordered_multiset
-
容器适配器 (Container Adaptors):提供特定接口的容器,底层由其他容器实现。
std::stack
std::queue
std::priority_queue
1. 顺序容器 (Sequence Containers)
std::vector
(三种数组的辨析与使用)
-
简介:动态数组。是在 C++ 中使用最广泛的容器。它能像数组一样快速随机访问,同时也能在末尾高效地添加或删除元素。
-
底层实现:一块连续的内存空间。当空间不足时,它会重新分配一块更大的内存(通常是原大小的 1.5 或 2 倍),并将所有旧元素复制到新空间,然后释放旧空间。
-
时间复杂度:
- 随机访问 (通过
[]
或.at()
): O(1) - 末尾添加/删除 (
push_back
,pop_back
): 均摊 O(1) - 中间或开头插入/删除: O(n) (因为需要移动后续所有元素)
- 随机访问 (通过
-
使用场景:
- 需要快速随机访问元素。
- 主要在容器末尾进行添加或删除操作。
- 当你需要一个动态数组时,
vector
应该是你的默认首选。
-
代码示例:
#include <iostream> #include <vector> int main() { std::vector<int> vec; vec.push_back(10); // [10] vec.push_back(20); // [10, 20] vec.push_back(30); // [10, 20, 30] std::cout << "第一个元素: " << vec[0] << std::endl; // 快速随机访问 vec.insert(vec.begin() + 1, 15); // 在中间插入 [10, 15, 20, 30] for (int val : vec) { std::cout << val << " "; } std::cout << std::endl; return 0; }
std::list
-
简介:双向链表。每个元素都存储了指向前一个和后一个元素的指针。
-
底层实现:非连续的节点,每个节点包含数据以及前后指针。
-
时间复杂度:
-
随机访问: O(n) (需要从头或尾遍历)
-
任意位置插入/删除: O(1) (前提是已持有指向该位置的迭代器)
-
-
使用场景:
-
在序列的任何位置都需要频繁地插入和删除操作。
-
不需要随机访问元素。
-
-
代码示例:
#include <iostream> #include <list> int main() { std::list<int> myList; myList.push_back(10); myList.push_front(5); // 在头部添加 myList.push_back(20); // [5, 10, 20] auto it = myList.begin(); it++; // 移动到 10 myList.insert(it, 8); // 在 10 前面插入 8, O(1) 操作 [5, 8, 10, 20] for (int val : myList) { std::cout << val << " "; } std::cout << std::endl; return 0; }
std::deque
(双端队列)
-
简介:Double-ended queue。结合了
vector
和list
的部分优点。支持快速随机访问,并且在序列的头部和尾部都能高效地添加或删除元素。 -
底层实现:分块的连续内存(一个指向多个内存块的指针数组)。它不是一块完整的连续内存,但每个块内部是连续的。
-
时间复杂度:
-
随机访问: O(1) (但常数时间比
vector
稍大) -
头部/末尾添加/删除 (
push_front
,pop_front
,push_back
,pop_back
): 均摊 O(1) -
中间插入/删除: O(n)
-
-
使用场景:
-
需要在容器的头部和尾部都进行频繁的插入和删除操作。
-
例如,实现一个工作窃取队列。
-
std::array
(C++11)
-
简介:固定大小的数组。是对 C 风格数组的封装,提供了更安全、更方便的接口(如获取大小、迭代器等)。
-
底层实现:与 C 风格数组完全一样,一块固定大小的连续内存,分配在栈上(如果作为局部变量)或静态存储区。
-
时间复杂度:
- 所有操作都与 C 风格数组相同。随机访问 O(1)。
-
使用场景:
-
当你明确知道需要一个固定大小的数组时。
-
需要 C 风格数组的性能,但又想要 STL 容器的便利性(如迭代器)。
-
2. 关联容器 (Associative Containers)
这类容器的共同特点是自动排序。
std::map
-
简介:存储键-值对 (
key-value
) 的集合,键是唯一的,并根据键自动排序。 -
底层实现:通常是红黑树 (Red-Black Tree),一种自平衡的二叉搜索树。
-
时间复杂度:
- 插入、删除、查找: O(logn)
-
使用场景:
-
需要存储键值对,并按键排序。
-
需要快速根据键查找值。
-
例如,存储学生姓名和分数的映射,并可以按姓名字典序输出。
-
-
代码示例:
#include <iostream> #include <map> #include <string> int main() { std::map<std::string, int> ageMap; ageMap["Alice"] = 30; ageMap["Bob"] = 25; ageMap.insert({"Charlie", 35}); std::cout << "Bob's age: " << ageMap["Bob"] << std::endl; // map 会自动按键排序 for (const auto& pair : ageMap) { std::cout << pair.first << ": " << pair.second << std::endl; } // 输出会是 Alice, Bob, Charlie 的顺序 return 0; }
std::set
-
简介:存储唯一元素的集合,并自动排序。
-
底层实现:与
std::map
类似,通常是红黑树。 -
时间复杂度:
- 插入、删除、查找: O(logn)
-
使用场景:
-
需要存储一组不重复的元素,并保持它们有序。
-
快速判断一个元素是否存在于集合中。
-
-
代码示例:
#include <iostream> #include <set> int main() { std::set<int> unique_numbers; unique_numbers.insert(10); unique_numbers.insert(5); unique_numbers.insert(10); // 重复的 10 不会被插入 if (unique_numbers.count(5)) { std::cout << "5 is in the set." << std::endl; } // set 会自动排序 for (int num : unique_numbers) { std::cout << num << " "; // 输出: 5 10 } std::cout << std::endl; return 0; }
-
std::multimap
和std::multiset
:分别是map
和set
的多键版本,允许存储重复的键。
3. 无序关联容器 (Unordered Associative Containers)
这类容器的共同特点是不排序,但提供更快的平均查找速度。
std::unordered_map
-
简介:存储键-值对的集合,键是唯一的,但元素是无序的。
-
底层实现:哈希表 (Hash Table)。
-
时间复杂度:
- 插入、删除、查找: 平均 O(1),最坏情况 O(n) (哈希冲突导致)
-
使用场景:
-
当你需要
map
的功能(键值对查找),但不关心元素的顺序,并且追求极致的查找性能时。这是map
的高性能替代品。 -
例如,统计一个文本文件中每个单词出现的频率。
-
-
代码示例:
#include <iostream> #include <unordered_map> #include <string> int main() { std::unordered_map<std::string, int> word_count; word_count["hello"]++; word_count["world"]++; word_count["hello"]++; std::cout << "Count of 'hello': " << word_count["hello"] << std::endl; // 遍历顺序是不确定的 for (const auto& pair : word_count) { std::cout << pair.first << ": " << pair.second << std::endl; } return 0; }
std::unordered_set
-
简介:存储唯一元素的集合,元素是无序的。
-
底层实现:哈希表 (Hash Table)。
-
时间复杂度:
- 插入、删除、查找: 平均 O(1),最坏情况 O(n)
-
使用场景:
-
当你需要
set
的功能(存储唯一元素),但不关心顺序,并且想要最快的查找速度。 -
例如,快速检查一个元素是否已经处理过。
-
-
std::unordered_multimap
和std::unordered_multiset
:允许存储重复键的哈希版本。
4. 容器适配器 (Container Adaptors)
它们不是真正的数据结构,而是对现有容器的接口进行封装,以提供特定的行为模式。
std::stack
(栈)
-
简介:后进先出 (Last-In, First-Out, LIFO) 的数据结构。
-
底层实现:默认使用
std::deque
,也可以用std::vector
或std::list
。 -
核心操作:
push()
(入栈),pop()
(出栈),top()
(查看栈顶元素)。所有操作都是 O(1)。 -
使用场景:函数调用栈、括号匹配、表达式求值、深度优先搜索 (DFS)。
-
代码示例:
#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 出栈 std::cout << "New top element: " << s.top() << std::endl; // 2 return 0; }
std::queue
(队列)
-
简介:先进先出 (First-In, First-Out, FIFO) 的数据结构。
-
底层实现:默认使用
std::deque
,也可以用std::list
。 -
核心操作:
push()
(入队),pop()
(出队),front()
(查看队首),back()
(查看队尾)。所有操作都是 O(1)。 -
使用场景:任务队列、消息队列、广度优先搜索 (BFS)。
-
代码示例:
#include <iostream> #include <queue> int main() { std::queue<int> q; q.push(1); // [1] q.push(2); // [1, 2] q.push(3); // [1, 2, 3] std::cout << "Front element: " << q.front() << std::endl; // 1 q.pop(); // 1 出队 std::cout << "New front element: " << q.front() << std::endl; // 2 return 0; }
std::priority_queue
(优先队列)
-
简介:一种特殊的队列,每次出队的总是当前队列中优先级最高(值最大或最小)的元素。
-
底层实现:默认使用
std::vector
作为底层容器,并以堆 (Heap) 的数据结构进行组织。 -
核心操作:
push()
(入队),pop()
(出队),top()
(查看优先级最高的元素)。push
和pop
都是 O(logn)。 -
使用场景:Dijkstra 算法、堆排序、任何需要动态找出最大/最小元素的场景。
-
代码示例:
#include <iostream> #include <queue> int main() { // 默认是最大堆 std::priority_queue<int> pq; pq.push(10); pq.push(30); pq.push(20); std::cout << "Top element (max): " << pq.top() << std::endl; // 30 pq.pop(); std::cout << "New top element: " << pq.top() << std::endl; // 20 // 创建最小堆 std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq; min_pq.push(10); min_pq.push(30); min_pq.push(20); std::cout << "Top element (min): " << min_pq.top() << std::endl; // 10 return 0; }
如何选择?一张速查表
需求 | 最佳选择 | 备选方案 | 为什么? |
---|---|---|---|
序列存储(默认) | std::vector | std::deque | 性能均衡,支持快速随机访问和尾部操作。 |
频繁在任意位置插入/删除 | std::list | O(1) 的插入/删除代价。 | |
固定大小的数组 | std::array | C 风格数组 | 更安全,接口更友好。 |
需要键值对映射,且按键排序 | std::map | 自动排序,查找速度 O(logn)。 | |
需要键值对映射,追求最快速度 | std::unordered_map | std::map | 平均 O(1) 查找,但无序。 |
需要存储唯一元素,并排序 | std::set | 自动去重并排序。 | |
需要存储唯一元素,追求最快速度 | std::unordered_set | std::set | 平均 O(1) 查找,用于快速判断存在性。 |
后进先出 (LIFO) | std::stack | 接口清晰,专为栈操作设计。 | |
先进先出 (FIFO) | std::queue | 接口清晰,专为队列操作设计。 | |
需要动态获取最大/最小元素 | std::priority_queue | 底层为堆,高效实现。 |
STL 之外的数据结构
除了 STL 提供的这些,还有一些重要的数据结构需要你自己实现或使用第三方库:
-
树 (Tree):如二叉树、AVL 树、B 树等。
std::map
和std::set
内部使用了红黑树,但没有直接提供树的容器。 -
图 (Graph):由顶点和边构成,用于表示网络关系。通常用邻接矩阵或邻接表实现。
-
字典树 (Trie):用于高效的字符串查找和前缀匹配。
-
布隆过滤器 (Bloom Filter):一种概率型数据结构,用于快速判断一个元素可能存在或绝对不存在于一个集合中。
以上是对所有数据结构进行的概括,具体每个数据结构的使用和语法会在后续文章中进一步描述。