这次我们来深入讲解 C++ STL 中的栈(stack)和队列(queue)容器。作为 C++ 标准模板库中最基础且实用的两种数据结构,栈和队列在算法实现、系统开发、游戏逻辑等场景中无处不在。掌握它们不仅能提升编程效率,更是面试和实际项目中的必备技能。
栈和队列的核心区别在于数据访问规则:栈遵循后进先出(LIFO),而队列遵循先进先出(FIFO)。STL 通过适配器模式实现了这两个容器,底层默认基于 deque(双端队列),也支持 list 或 vector 作为底层容器。本文将重点介绍它们的基本操作、使用场景、性能特点和实际代码示例。
1. 核心能力速览
| 能力项 | 栈(stack) | 队列(queue) |
|---|---|---|
| 数据结构特性 | 后进先出(LIFO) | 先进先出(FIFO) |
| 头文件 | #include <stack> | #include <queue> |
| 底层默认容器 | deque | deque |
| 主要操作 | push, pop, top, empty, size | push, pop, front, back, empty, size |
| 时间复杂度 | 所有操作 O(1) | 所有操作 O(1) |
| 适用场景 | 函数调用栈、表达式求值、撤销操作 | 消息队列、任务调度、广度优先搜索 |
2. 栈(stack)深度解析
栈是一种限制性线性表,只允许在表的一端进行插入和删除操作,这一端称为栈顶。STL stack 提供了一套简洁的接口来管理这种 LIFO 结构。
2.1 栈的基本操作
#include <iostream> #include <stack> using namespace std; int main() { stack<int> s; // 入栈操作 s.push(10); s.push(20); s.push(30); cout << "栈大小: " << s.size() << endl; // 输出: 3 cout << "栈顶元素: " << s.top() << endl; // 输出: 30 // 出栈操作 s.pop(); cout << "弹出后栈顶: " << s.top() << endl; // 输出: 20 // 检查栈是否为空 while (!s.empty()) { cout << s.top() << " "; s.pop(); } // 输出: 20 10 return 0; }2.2 栈的典型应用场景
表达式求值是栈的经典应用。以下是一个简单的括号匹配检查器:
#include <stack> #include <string> #include <iostream> using namespace std; bool isValidParentheses(const string& expr) { stack<char> s; for (char c : expr) { if (c == '(' || c == '[' || c == '{') { s.push(c); } else { if (s.empty()) return false; char top = s.top(); if ((c == ')' && top == '(') || (c == ']' && top == '[') || (c == '}' && top == '{')) { s.pop(); } else { return false; } } } return s.empty(); } int main() { string test1 = "({[]})"; // 有效 string test2 = "({[}])"; // 无效 cout << test1 << ": " << isValidParentheses(test1) << endl; // 输出: 1 cout << test2 << ": " << isValidParentheses(test2) << endl; // 输出: 0 return 0; }浏览器前进后退功能也是栈的典型应用。用户访问的页面被压入栈中,后退时从栈中弹出:
class BrowserHistory { private: stack<string> backStack; stack<string> forwardStack; string current; public: BrowserHistory(string homepage) : current(homepage) {} void visit(string url) { backStack.push(current); current = url; // 清空前向栈(新访问时前向历史失效) while (!forwardStack.empty()) forwardStack.pop(); } string back(int steps) { while (steps-- > 0 && !backStack.empty()) { forwardStack.push(current); current = backStack.top(); backStack.pop(); } return current; } string forward(int steps) { while (steps-- > 0 && !forwardStack.empty()) { backStack.push(current); current = forwardStack.top(); forwardStack.pop(); } return current; } };3. 队列(queue)全面掌握
队列是一种先进先出的线性表,只允许在表的前端进行删除操作,在表的后端进行插入操作。STL queue 完美实现了这种 FIFO 特性。
3.1 队列的基本操作
#include <iostream> #include <queue> using namespace std; int main() { queue<int> q; // 入队操作 q.push(10); q.push(20); q.push(30); cout << "队列大小: " << q.size() << endl; // 输出: 3 cout << "队首元素: " << q.front() << endl; // 输出: 10 cout << "队尾元素: " << q.back() << endl; // 输出: 30 // 出队操作 q.pop(); cout << "出队后队首: " << q.front() << endl; // 输出: 20 // 遍历队列(注意:队列不支持迭代器,需要边出队边访问) while (!q.empty()) { cout << q.front() << " "; q.pop(); } // 输出: 20 30 return 0; }3.2 队列的典型应用场景
消息队列是队列在系统开发中的重要应用。以下是一个简单的任务调度器示例:
#include <queue> #include <string> #include <iostream> #include <thread> #include <chrono> using namespace std; class TaskScheduler { private: queue<string> taskQueue; public: void addTask(const string& task) { taskQueue.push(task); cout << "添加任务: " << task << endl; } void processTasks() { while (!taskQueue.empty()) { string task = taskQueue.front(); taskQueue.pop(); cout << "处理任务: " << task << endl; // 模拟任务处理时间 this_thread::sleep_for(chrono::seconds(1)); } cout << "所有任务处理完成!" << endl; } }; int main() { TaskScheduler scheduler; scheduler.addTask("备份数据库"); scheduler.addTask("发送邮件通知"); scheduler.addTask("生成报表"); scheduler.processTasks(); return 0; }广度优先搜索(BFS)是队列在算法中的核心应用。以下是一个简单的图遍历示例:
#include <queue> #include <vector> #include <iostream> using namespace std; void BFS(vector<vector<int>>& graph, int start) { vector<bool> visited(graph.size(), false); queue<int> q; visited[start] = true; q.push(start); cout << "BFS遍历顺序: "; while (!q.empty()) { int node = q.front(); q.pop(); cout << node << " "; for (int neighbor : graph[node]) { if (!visited[neighbor]) { visited[neighbor] = true; q.push(neighbor); } } } cout << endl; } int main() { // 图的邻接表表示 vector<vector<int>> graph = { {1, 2}, // 节点0的邻居 {0, 3, 4}, // 节点1的邻居 {0, 5}, // 节点2的邻居 {1}, // 节点3的邻居 {1}, // 节点4的邻居 {2} // 节点5的邻居 }; BFS(graph, 0); // 从节点0开始BFS return 0; }4. 底层容器选择与性能优化
STL 栈和队列是容器适配器,这意味着它们基于其他序列容器实现。理解底层容器的选择对性能优化至关重要。
4.1 自定义底层容器
#include <stack> #include <queue> #include <vector> #include <list> #include <iostream> using namespace std; int main() { // 基于vector的栈 stack<int, vector<int>> s_vec; s_vec.push(1); s_vec.push(2); // 基于list的队列 queue<int, list<int>> q_list; q_list.push(10); q_list.push(20); cout << "vector栈顶: " << s_vec.top() << endl; // 输出: 2 cout << "list队列首: " << q_list.front() << endl; // 输出: 10 return 0; }4.2 不同底层容器的性能对比
| 底层容器 | 栈适用性 | 队列适用性 | 内存使用 | 访问性能 |
|---|---|---|---|---|
| deque(默认) | 优秀 | 优秀 | 中等 | 稳定 O(1) |
| vector | 优秀 | 不适用 | 紧凑 | 尾操作 O(1) |
| list | 良好 | 优秀 | 较高 | 稳定 O(1) |
选择建议:
- 大多数情况下使用默认的 deque
- 需要紧凑内存且只用于栈时选择 vector
- 需要频繁中间插入删除时选择 list
5. 高级应用:单调栈与单调队列
单调栈和单调队列是解决特定问题的强大工具,在算法竞赛和面试中经常出现。
5.1 单调栈应用:下一个更大元素
#include <vector> #include <stack> #include <iostream> using namespace std; vector<int> nextGreaterElement(const vector<int>& nums) { int n = nums.size(); vector<int> result(n, -1); stack<int> s; // 存储索引的单调递减栈 for (int i = 0; i < n; i++) { while (!s.empty() && nums[i] > nums[s.top()]) { result[s.top()] = nums[i]; s.pop(); } s.push(i); } return result; } int main() { vector<int> nums = {2, 1, 2, 4, 3}; vector<int> result = nextGreaterElement(nums); cout << "原数组: "; for (int num : nums) cout << num << " "; cout << "\n下一个更大元素: "; for (int res : result) cout << res << " "; // 输出: 原数组: 2 1 2 4 3 // 下一个更大元素: 4 2 4 -1 -1 return 0; }5.2 单调队列应用:滑动窗口最大值
#include <vector> #include <deque> #include <iostream> using namespace std; vector<int> maxSlidingWindow(const vector<int>& nums, int k) { if (nums.empty()) return {}; vector<int> result; deque<int> dq; // 存储索引的单调递减队列 for (int i = 0; i < nums.size(); i++) { // 移除超出窗口范围的元素 if (!dq.empty() && dq.front() == i - k) { dq.pop_front(); } // 维护单调递减性 while (!dq.empty() && nums[i] >= nums[dq.back()]) { dq.pop_back(); } dq.push_back(i); // 当窗口形成时记录最大值 if (i >= k - 1) { result.push_back(nums[dq.front()]); } } return result; } int main() { vector<int> nums = {1, 3, -1, -3, 5, 3, 6, 7}; int k = 3; vector<int> result = maxSlidingWindow(nums, k); cout << "滑动窗口最大值: "; for (int num : result) cout << num << " "; // 输出: 3 3 5 5 6 7 return 0; }6. 常见问题与解决方案
6.1 空容器访问错误
// 错误示例 stack<int> s; s.pop(); // 未定义行为,可能崩溃 cout << s.top(); // 同样危险 // 正确做法 if (!s.empty()) { s.pop(); cout << s.top(); }6.2 迭代器使用限制
queue<int> q; q.push(1); q.push(2); // 错误:队列没有迭代器 // for (auto it = q.begin(); it != q.end(); ++it) // 正确:通过出队方式遍历 while (!q.empty()) { cout << q.front() << " "; q.pop(); }6.3 性能优化技巧
预分配空间(对于基于vector的栈):
stack<int, vector<int>> s; // 如果知道大致大小,可以预先reserve // 但需要直接操作底层容器: s.get_container().reserve(1000);批量操作优化:
// 批量入栈比单个入栈更高效 vector<int> data = {1, 2, 3, 4, 5}; stack<int> s; for (int num : data) { s.push(num); // 避免多次函数调用开销 }7. 实际项目中的最佳实践
7.1 线程安全考虑
STL 容器本身不是线程安全的。在多线程环境中使用栈和队列时需要额外的同步机制:
#include <mutex> #include <stack> template<typename T> class ThreadSafeStack { private: stack<T> data; mutable mutex mtx; public: void push(T value) { lock_guard<mutex> lock(mtx); data.push(std::move(value)); } bool pop(T& value) { lock_guard<mutex> lock(mtx); if (data.empty()) return false; value = std::move(data.top()); data.pop(); return true; } bool empty() const { lock_guard<mutex> lock(mtx); return data.empty(); } };7.2 内存管理建议
对于大型数据对象,考虑使用智能指针避免拷贝开销:
#include <memory> #include <stack> class LargeObject { // 假设这是一个大数据对象 }; int main() { stack<shared_ptr<LargeObject>> objStack; objStack.push(make_shared<LargeObject>()); // 共享所有权,避免大型对象拷贝 return 0; }7.3 错误处理策略
实现健壮的栈和队列包装器,提供更好的错误信息:
template<typename T> class SafeStack { private: stack<T> s; public: T safePop() { if (s.empty()) { throw runtime_error("尝试从空栈弹出元素"); } T value = s.top(); s.pop(); return value; } T safeTop() const { if (s.empty()) { throw runtime_error("尝试访问空栈顶部"); } return s.top(); } };8. 性能测试与对比分析
为了帮助读者更好地理解不同实现的性能特点,我们设计一个简单的性能测试:
#include <chrono> #include <iostream> #include <stack> #include <queue> void benchmarkStack(int operations) { stack<int> s; auto start = chrono::high_resolution_clock::now(); for (int i = 0; i < operations; i++) { s.push(i); } for (int i = 0; i < operations; i++) { s.pop(); } auto end = chrono::high_resolution_clock::now(); auto duration = chrono::duration_cast<chrono::microseconds>(end - start); cout << "栈操作 " << operations << " 次耗时: " << duration.count() << " 微秒" << endl; } void benchmarkQueue(int operations) { queue<int> q; auto start = chrono::high_resolution_clock::now(); for (int i = 0; i < operations; i++) { q.push(i); } for (int i = 0; i < operations; i++) { q.pop(); } auto end = chrono::high_resolution_clock::now(); auto duration = chrono::duration_cast<chrono::microseconds>(end - start); cout << "队列操作 " << operations << " 次耗时: " << duration.count() << " 微秒" << endl; } int main() { benchmarkStack(100000); benchmarkQueue(100000); return 0; }栈和队列作为 C++ STL 的基础组件,其设计简洁而强大。掌握它们不仅意味着能够处理 LIFO 和 FIFO 场景,更重要的是理解容器适配器的设计思想。在实际项目中,合理选择底层容器、注意线程安全、优化内存使用,都能显著提升代码质量和性能表现。
对于需要处理更复杂场景的开发者,可以进一步研究 priority_queue(优先队列)和 deque(双端队列),它们提供了更多的灵活性和功能。栈和队列的掌握程度直接影响到算法实现和系统设计的质量,建议通过实际编码练习来加深理解。