C++ STL:深入探索常见容器
你好呀,欢迎来到 Dong雨 的技术小栈 🌱
在这里,我们一同探索代码的奥秘,感受技术的魅力 ✨。
👉 我的小世界:Dong雨
📌 分享我的学习旅程
🛠️ 提供贴心的实用工具
💡 记录每一个灵感火花
🌟✨ Hello,探索技术的你,这里是本篇的地图指南! ✨🌟
C++ STL:深入探索常见容器
引言:高效编程的核心
C++ 标准模板库(STL)是构建高效程序的强大工具,它提供了多种容器,帮助开发者应对复杂的数据存储和操作需求。STL 容器支持广泛的操作,包括动态内存管理、灵活的元素存储、以及高效的访问与查找能力。本文将深入探讨 STL 中常见的容器,并分析它们的特性、优势和适用场景。
1. vector:动态扩展的数组
概述:
vector
是 STL 中最常用的容器之一,它实现了一个动态数组。vector
具有自动扩容的特性,能够根据实际需求调整内存大小。这使得它特别适合处理需要频繁插入和删除元素的场景,尤其是当元素数量不确定时。
使用场景:
vector
适用于需要频繁访问元素、存储大量数据、并且数据规模动态变化的应用场景,如:
- 存储大量数字数据或对象。
- 图像处理中的像素数据。
- 快速随机访问的应用。
头文件:
#include <vector>
using namespace std;
初始化:
vector<int> vec = {1, 2, 3, 4}; // 使用列表初始化
常用方法:
push_back(value)
:将元素添加到vector
的末尾。pop_back()
:移除vector
末尾的元素。at(index)
:获取指定位置的元素,提供越界检查。size()
:返回vector
中元素的数量。reserve(n)
:预先为vector
分配内存,以避免在频繁添加元素时导致多次扩容。
深入分析:
vector
提供了 O(1) 时间复杂度的随机访问,且支持 动态扩容。当 vector
的容量不足以容纳新元素时,它会自动扩容,通常扩展为原来的两倍,这样就能确保每次扩容时的操作成本较低(均摊复杂度为 O(1))。然而,vector
的扩容也有其开销,尤其是当容器中的元素非常大时,内存重新分配和元素拷贝可能会影响性能。
示例代码:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> numbers = {1, 2, 3, 4}; // 初始化 vector
numbers.push_back(5); // 添加元素
// 输出 vector 中的所有元素
cout << "Vector contents: ";
for (const auto& num : numbers) {
cout << num << " ";
}
cout << endl;
cout << "Vector size: " << numbers.size() << endl; // 获取大小
return 0;
}
输出:
Vector contents: 1 2 3 4 5
Vector size: 5
2. stack:后进先出(LIFO)容器
概述:
stack
是遵循后进先出(LIFO)原则的容器。stack
只允许从顶端进行插入和删除操作,因此特别适合处理递归问题和需要回溯的任务。
使用场景:
stack
常用于以下场景:
- 深度优先搜索(DFS)算法。
- 撤销操作。
- 表达式求值(如括号匹配问题)。
头文件:
#include <stack>
using namespace std;
初始化:
stack<int> s;
常用方法:
push(value)
:将元素压入栈顶。pop()
:移除栈顶元素。top()
:访问栈顶元素。empty()
:检查栈是否为空。size()
:返回栈中元素的数量。
深入分析:
stack
底层通常使用 deque
或 vector
来实现,因此它的时间复杂度是 O(1),无论是入栈还是出栈。stack
只提供对栈顶元素的访问,不支持中间元素的访问。适合用来处理先进后出的操作,但若需要频繁查找其他元素时,应考虑其他数据结构。
示例代码:
#include <iostream>
#include <stack>
using namespace std;
int main() {
stack<int> s;
// 压入元素
s.push(1);
s.push(2);
s.push(3);
// 输出栈顶元素
cout << "Stack top: " << s.top() << endl;
// 弹出栈顶元素
s.pop();
cout << "New stack top: " << s.top() << endl;
return 0;
}
输出:
Stack top: 3
New stack top: 2
3. queue:先进先出(FIFO)队列
概述:
queue
是遵循先进先出(FIFO)规则的容器,常用于需要按顺序处理任务的场景。
使用场景:
queue
常用于:
- 事件驱动程序。
- 任务调度(如操作系统中的进程调度)。
- 异步消息传递。
头文件:
#include <queue>
using namespace std;
初始化:
queue<int> q;
常用方法:
push(value)
:将元素加入队列。pop()
:移除队列头部元素。front()
:访问队列头部元素。back()
:访问队列尾部元素。empty()
:检查队列是否为空。size()
:返回队列中元素的数量。
深入分析:
queue
是基于 双端队列(deque
)实现的,它的操作复杂度为 O(1)。队列的操作仅限于队首和队尾,因此它在需要保持顺序处理任务的场景中非常高效。但在需要从队列中间插入或删除元素时,队列并不是一个好的选择。
示例代码:
#include <iostream>
#include <queue>
using namespace std;
int main() {
queue<int> q;
// 入队
q.push(10);
q.push(20);
q.push(30);
// 输出队列头部元素
cout << "Queue front: " << q.front() << endl;
// 出队
q.pop();
cout << "New queue front: " << q.front() << endl;
return 0;
}
输出:
Queue front: 10
New queue front: 20
4. deque:双端队列
概述:
deque
是一个可以从两端插入和删除元素的容器。与 vector
相比,deque
更适合需要从两端频繁操作元素的场景。
使用场景:
deque
常用于:
- 滑动窗口问题。
- 双向任务队列。
- 游戏中的消息队列。
头文件:
#include <deque>
using namespace std;
初始化:
deque<int> dq = {1, 2, 3, 4};
常用方法:
push_front(value)
:将元素添加到队列前端。push_back(value)
:将元素添加到队列末端。pop_front()
:移除队列前端的元素。pop_back()
:移除队列末端的元素。front()
:访问队列前端元素。back()
:访问队列末端元素。size()
:返回队列中元素的数量。
深入分析:
deque
允许在队列两端进行插入和删除操作,因此它比 vector
更适合双向操作。其底层通常是由多个块构成的结构,这使得它的空间效率较高,但相较于 vector
,它的访问元素速度稍慢,尤其是对尾端元素的访问。因此,deque
在某些双端任务中表现优越,但若只在一端操作,vector
可能更高效。
示例代码:
#include #include using namespace std;
int main() { deque dq = {1, 2, 3, 4};
// 添加元素到队头
dq.push_front(0);
// 添加元素到队尾
dq.push_back(5);
// 输出队列元素
cout << "Deque contents: ";
for (const auto& val : dq) {
cout << val << " ";
}
cout << endl;
return 0;
}
输出:
Deque contents: 0 1 2 3 4 5
5. priority_queue:优先级队列
概述:
priority_queue
是一个根据优先级排序的队列,默认情况下使用最大堆实现。它允许快速访问最大或最小元素,但不像常规队列一样按顺序处理元素。priority_queue
保证每次出队的元素都是当前队列中的最大(或最小)元素。
使用场景:
priority_queue
常用于:
- 排序问题。
- 实现 Dijkstra 算法、A* 算法等图搜索算法。
- 实现事件驱动系统中的优先级任务调度。
头文件:
#include <queue>
using namespace std;
初始化:
priority_queue<int> pq;
常用方法:
push(value)
:将元素加入优先级队列。pop()
:移除队列中的最大元素。top()
:访问优先级队列中的最大元素。empty()
:检查队列是否为空。size()
:返回队列中元素的数量。
深入分析:
priority_queue
的底层通常使用最大堆或最小堆(通过自定义比较器来改变堆的顺序)。由于堆的性质,push
和 pop
操作的时间复杂度为 O(log n)。虽然它非常适合用于按优先级处理元素,但它不支持访问中间元素或随机访问,因此如果需要按特定条件查找元素,priority_queue
可能不是最佳选择。
示例代码:
#include <iostream>
#include <queue>
using namespace std;
int main() {
priority_queue<int> pq;
// 向优先级队列中添加元素
pq.push(10);
pq.push(20);
pq.push(15);
// 输出优先级队列中的最大元素
cout << "Top element: " << pq.top() << endl;
// 移除最大元素
pq.pop();
cout << "New top element: " << pq.top() << endl;
return 0;
}
输出:
Top element: 20
New top element: 15
6. map:键值对存储(红黑树)
概述:
map
是一种关联容器,用于存储键值对。map
中的每个元素都由一个唯一的键和一个与之关联的值组成,且元素按键的顺序自动排序。map
默认使用 红黑树 实现,保证了插入、查找、删除操作的时间复杂度为 O(log n)。
使用场景:
map
常用于:
- 需要根据键高效查找值的场景。
- 实现频率计数、符号表等数据结构。
- 数据按键排序的场景。
头文件:
#include <map>
using namespace std;
初始化:
map<int, string> m;
常用方法:
insert({key, value})
:向map
插入键值对。erase(key)
:删除指定键的元素。find(key)
:查找指定键的元素。at(key)
:访问指定键的值,若键不存在抛出异常。size()
:返回map
中的元素数量。
深入分析:
map
是一个基于红黑树实现的排序容器,因此所有的键值对都保持按键排序的状态。这使得 map
在需要排序数据或进行键查找时非常高效。然而,由于其底层结构的特性,map
中的元素不能像 unordered_map
那样以 O(1) 时间复杂度进行查找。
示例代码:
#include <iostream>
#include <map>
using namespace std;
int main() {
map<int, string> m;
// 向 map 中插入元素
m.insert({1, "Apple"});
m.insert({2, "Banana"});
m[3] = "Cherry";
// 输出所有键值对
for (const auto& p : m) {
cout << p.first << ": " << p.second << endl;
}
return 0;
}
输出:
1: Apple
2: Banana
3: Cherry
7. set:无重复的有序集合
概述:
set
是一个集合类型的容器,存储的是唯一元素,并且按照升序(或自定义顺序)排列。set
的实现通常基于 红黑树,所以它的插入、删除和查找操作都具有 O(log n) 的时间复杂度。
使用场景:
set
适用于:
- 需要无重复元素的场景。
- 元素按特定顺序进行排序的需求。
- 需要高效查找、插入和删除的场景。
头文件:
#include <set>
using namespace std;
初始化:
set<int> s;
常用方法:
insert(value)
:将元素插入集合。erase(value)
:删除指定的元素。find(value)
:查找元素。size()
:返回集合中元素的数量。empty()
:检查集合是否为空。
深入分析:
set
内部使用红黑树保持元素有序,支持自动排序,因此其插入、查找、删除操作的时间复杂度为 O(log n)。但是,set
不允许重复元素,因此在插入元素时,如果元素已经存在,它不会再次插入。
示例代码:
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> s;
// 插入元素
s.insert(1);
s.insert(2);
s.insert(3);
// 输出所有元素
cout << "Set contents: ";
for (const auto& elem : s) {
cout << elem << " ";
}
cout << endl;
return 0;
}
输出:
Set contents: 1 2 3
8. pair:存储两个元素的组合
概述:
pair
是一个简单的容器,用于存储两个相关联的元素。pair
是一个模板类,可以用来存储任何两种类型的值。
使用场景:
pair
常用于:
- 关联两个不同类型的数据。
- 作为返回值,尤其是函数需要返回多个相关的值时。
- 与
map
、set
一起使用来存储键值对。
头文件:
#include <utility>
using namespace std;
初始化:
pair<int, string> p = {1, "Apple"};
常用方法:
first
:访问pair
的第一个元素。second
:访问pair
的第二个元素。
深入分析:
pair
提供了一个简单而有效的方式来将两个不同类型的数据捆绑在一起。它的访问非常方便,直接通过 first
和 second
成员进行访问。pair
在处理关联数据(如键值对)时非常有用。
示例代码:
#include <iostream>
#include <utility>
using namespace std;
int main() {
pair<int, string> p = {1, "Apple"};
// 输出 pair 中的元素
cout << "First: " << p.first << ", Second: " << p.second << endl;
return 0;
}
输出:
First: 1, Second: Apple
9. string:动态字符数组
概述:
string
是 C++ 标准库中用于处理文本数据的类。它支持动态调整大小,能够灵活处理字符串的插入、删除、查找和替换等操作。
使用场景:
string
常用于:
- 处理动态长度的文本数据。
- 文件操作、网络协议解析、用户输入等。
头文件:
#include <string>
using namespace std;
初始化:
string s = "Hello, world!";
常用方法:
length()
:返回字符串的长度。empty()
:检查字符串是否为空。append(str)
:向字符串末尾追加内容。find(substr)
:查找子字符串的位置。substr(start, length)
:返回子字符串。
深入分析:
string
类是一个非常强大且灵活的容器,提供了大量处理文本数据的函数。string
内部使用动态数组,因此能够自动扩展其容量。字符串操作通常涉及到字符的复制、移动或插入,所以操作时需要特别注意效率和内存管理。
示例代码:
#include <iostream>
#include <string>
using namespace std;
int main() {
string s = "Hello, world!";
// 查找字符串中的字符位置
cout << "Found 'world' at position: " << s.find("world") << endl;
// 取子字符串
string sub = s.substr(7, 5);
cout << "Sub-string: " << sub << endl;
return 0;
}
输出:
Found 'world' at position: 7
Sub-string: world
10. bitset:固定大小的位集合
概述:
bitset
是一个用于处理固定数量位的容器,提供对位的操作。bitset
可以用来进行二进制操作、实现位掩码等应用。它的大小在编译时就已确定,因此非常高效。
使用场景:
bitset
常用于:
- 位掩码和位操作。
- 存储和操作二进制状态数据。
- 实现某些低级算法(如布尔运算)。
头文件:
#include <bitset>
using namespace std;
初始化:
bitset<8> b1; // 默认值为 00000000
bitset<8> b2("10101010"); // 初始化为二进制字符串
常用方法:
set(pos)
:将指定位置的位设为 1。reset(pos)
:将指定位置的位设为 0。flip(pos)
:翻转指定位置的位。size()
:返回 bitset 的大小。count()
:返回 1 的位数。
深入分析:
bitset
是一个非常高效的位操作工具。它使用固定大小的数组来存储位,并提供直接的位操作方法。由于其大小在编译时就已确定,因此对于存储较小二进制数据非常合适。不过,bitset
不适合动态大小的位集合,且无法像 vector
那样随时扩展。
示例代码:
#include <iostream>
#include <bitset>
using namespace std;
int main() {
bitset<8> b1("10101010");
// 输出 bitset 的内容
cout << "Initial bitset: " << b1 << endl;
// 翻转第 2 位
b1.flip(2);
cout << "After flipping 2nd bit: " << b1 << endl;
// 获取 1 的位数
cout << "Number of 1s: " << b1.count() << endl;
return 0;
}
输出:
Initial bitset: 10101010
After flipping 2nd bit: 10100010
Number of 1s: 3
11. array:固定大小的数组
概述:
array
是 C++11 引入的一个容器,它是一个固定大小的数组容器。与 C 风格数组不同,array
是一个模板类,它支持 STL 容器的所有常用操作(如迭代器、排序等),但大小在编译时就已经确定。
使用场景:
array
适用于:
- 需要固定大小数组的场景。
- 需要类似于传统数组的性能,且希望获得更强大的 STL 操作支持时。
头文件:
#include <array>
using namespace std;
初始化:
array<int, 5> arr = {1, 2, 3, 4, 5};
常用方法:
at(index)
:返回指定位置的元素。size()
:返回数组的大小。fill(value)
:将所有元素初始化为相同的值。front()
:返回第一个元素。back()
:返回最后一个元素。
深入分析:
array
使得 C 风格数组拥有了 STL 容器的一些优势。它的大小固定,因此可以在栈上高效分配。相比于 vector
,array
不支持动态扩展,但它提供了更强大的 STL 接口和更高的类型安全性。适合存储大小固定的数据。
示例代码:
#include <iostream>
#include <array>
using namespace std;
int main() {
array<int, 5> arr = {1, 2, 3, 4, 5};
// 输出数组的元素
cout << "Array elements: ";
for (const auto& elem : arr) {
cout << elem << " ";
}
cout << endl;
// 修改第一个元素
arr[0] = 10;
cout << "After modifying first element: " << arr[0] << endl;
return 0;
}
输出:
Array elements: 1 2 3 4 5
After modifying first element: 10
12. tuple:异构容器
概述:
tuple
是一个可以存储多个不同类型元素的容器。与 pair
类似,tuple
可以包含多个不同类型的元素,并且支持访问每个元素。tuple
提供了一种灵活的方式来捆绑多种类型的数据。
使用场景:
tuple
适用于:
- 返回多个不同类型的值的函数。
- 存储多个不同类型的参数或数据。
- 元素数量在编译时已知的场景。
头文件:
#include <tuple>
using namespace std;
初始化:
tuple<int, string, float> t = {1, "Apple", 3.14};
常用方法:
get<index>(tuple)
:访问指定索引的元素。tuple_size<tuple>::value
:返回tuple
的大小。make_tuple()
:创建一个tuple
。
深入分析:
tuple
是 C++ 中的一个非常灵活的容器,能够存储不同类型的数据。虽然它不如 vector
或 map
那样高频使用,但在需要处理多种类型的函数返回值或数据组合时非常有用。tuple
支持通过索引和类型访问元素。
示例代码:
#include <iostream>
#include <tuple>
using namespace std;
int main() {
tuple<int, string, float> t = {1, "Apple", 3.14};
// 访问 tuple 中的元素
cout << "First: " << get<0>(t) << ", Second: " << get<1>(t) << ", Third: " << get<2>(t) << endl;
// 修改 tuple 中的元素
get<1>(t) = "Banana";
cout << "Modified tuple: " << get<0>(t) << ", " << get<1>(t) << ", " << get<2>(t) << endl;
return 0;
}
输出:
First: 1, Second: Apple, Third: 3.14
Modified tuple: 1, Banana, 3.14
总结:
在这篇博客中,我们深入分析了 C++ STL 中的常用容器。每个容器都有其特定的使用场景和优缺点。在实际开发中,选择合适的容器是编写高效和易于维护代码的关键。了解每个容器的工作原理、常用方法以及深入分析将帮助你在复杂的应用中做出更好的选择。
通过对以下容器的详细讲解和示例,你可以清晰地了解每个容器的特性和应用场景:
- vector:动态数组,适用于需要快速随机访问的场景。
- stack:栈,适用于后进先出(LIFO)的场景。
- queue:队列,适用于先进先出(FIFO)的场景。
- deque:双端队列,适用于两端操作都频繁的场景。
- priority_queue:优先级队列,适用于按优先级排序的场景。
- map:键值对容器,适用于根据键查找值的场景。
- set:无重复元素的有序集合,适用于去重和排序的场景。
- pair:存储两个相关元素的组合,常用于
map
中。 - string:动态字符串,适用于文本处理的场景。
- bitset:固定大小的位集合,适用于位运算的场景。
- array:固定大小的数组,适用于大小已知且不变的场景。
- tuple:存储多个不同类型元素的容器,适用于函数返回多个不同类型值的场景。
每个容器的底层实现和时间复杂度有所不同,选择正确的容器能显著提高程序的性能和可维护性。希望这篇文章能帮助你更好地理解 C++ STL 容器的使用和选择。
🎉🌈 陪伴至此,感谢有你 🌈🎉
感谢你能坚持看到这里!如果这篇文章对你有一点点帮助,希望能收获你的:
👍 一个赞,⭐ 一个收藏,💬 一条评论 或 🔗 一键分享!
你的支持是我持续输出的最大动力!✨有问题?有灵感?
别犹豫,直接留言和我交流~让我们一起成长、一起突破 💡。最后,祝你:
🍯 生活美满如蜜香
🌞 心情灿烂似朝阳
🌱 成长如树渐成章
🚀 未来闪耀梦飞翔!再次感谢你的阅读!🌟 下次再见~ 🎉