1. 理解stack和queue的基本概念
在C++标准模板库(STL)中,stack和queue是两种最常用的容器适配器。它们不是独立的容器,而是基于其他容器实现的接口包装器。理解这一点对于正确使用它们至关重要。
stack(栈)遵循LIFO(Last In First Out,后进先出)原则,就像一摞盘子,你只能从最上面放入或取出。queue(队列)则遵循FIFO(First In First Out,先进先出)原则,类似于排队买票,先来的人先得到服务。
注意:虽然stack和queue默认分别基于deque和list实现,但你可以指定底层容器类型。这是很多初学者容易忽略的灵活性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. stack的深度解析与实战应用
2.1 stack的核心操作
stack的接口极为简洁,主要包含以下几个关键操作:
- push():将元素压入栈顶
- pop():移除栈顶元素(不返回该元素)
- top():访问栈顶元素(不移除)
- empty():检查栈是否为空
- size():获取栈中元素数量
cpp复制#include <stack>
#include <iostream>
int main() {
std::stack<int> s;
s.push(1); // 栈:1
s.push(2); // 栈:2 1
s.push(3); // 栈:3 2 1
std::cout << "Top element: " << s.top() << std::endl; // 输出3
s.pop(); // 移除3
std::cout << "Top element now: " << s.top() << std::endl; // 输出2
return 0;
}
2.2 stack的底层实现选择
默认情况下,stack使用deque作为底层容器,但你也可以指定其他容器:
cpp复制#include <stack>
#include <vector>
#include <list>
std::stack<int, std::vector<int>> vec_stack; // 基于vector实现
std::stack<int, std::list<int>> list_stack; // 基于list实现
选择不同底层容器会影响性能:
- vector:内存连续,随机访问快,但增长时可能需要重新分配内存
- list:内存不连续,插入删除快,但内存开销大
- deque(默认):折中方案,支持快速首尾操作
2.3 stack的经典应用场景
- 括号匹配检查:编译器常用stack来检查代码中的括号是否匹配
- 表达式求值:将中缀表达式转换为后缀表达式(逆波兰表示法)
- 函数调用栈:程序执行时的函数调用关系就是用栈管理的
- 撤销操作:许多软件的撤销功能使用栈来保存操作历史
3. queue的深入剖析与实际使用
3.1 queue的基本操作
queue提供以下主要接口:
- push():在队尾插入元素
- pop():移除队首元素
- front():访问队首元素
- back():访问队尾元素
- empty():检查队列是否为空
- size():获取队列中元素数量
cpp复制#include <queue>
#include <iostream>
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
std::cout << "Back element: " << q.back() << std::endl; // 输出3
q.pop(); // 移除1
std::cout << "Front element now: " << q.front() << std::endl; // 输出2
return 0;
}
3.2 queue的底层容器选择
与stack类似,queue也可以指定不同的底层容器:
cpp复制#include <queue>
#include <list>
std::queue<int> default_queue; // 默认基于deque
std::queue<int, std::list<int>> list_queue; // 基于list实现
重要限制:queue不能基于vector实现,因为vector不支持高效的头部删除操作
3.3 queue的典型应用
- BFS算法:广度优先搜索中使用队列管理待访问节点
- 消息队列:多线程/进程通信中的任务调度
- 打印机队列:管理多个打印任务的执行顺序
- 缓存系统:实现FIFO缓存淘汰策略
4. stack和queue的高级用法与性能考量
4.1 自定义stack和queue
你可以基于现有容器创建具有特殊功能的stack或queue。例如,一个能获取最小元素的栈:
cpp复制#include <stack>
class MinStack {
private:
std::stack<int> data_stack;
std::stack<int> min_stack;
public:
void push(int x) {
data_stack.push(x);
if(min_stack.empty() || x <= min_stack.top()) {
min_stack.push(x);
}
}
void pop() {
if(data_stack.top() == min_stack.top()) {
min_stack.pop();
}
data_stack.pop();
}
int top() {
return data_stack.top();
}
int getMin() {
return min_stack.top();
}
};
4.2 性能对比与选择建议
| 操作 | stack (基于deque) | queue (基于list) | 备注 |
|---|---|---|---|
| 插入 | O(1) | O(1) | 两者都很快 |
| 删除 | O(1) | O(1) | stack是pop_top, queue是pop_front |
| 访问顶部元素 | O(1) | O(1) | stack是top, queue是front |
| 内存使用 | 中等 | 较高 | list的节点开销较大 |
选择建议:
- 如果只需要LIFO操作,优先使用stack
- 如果需要FIFO操作,使用queue
- 如果需要两端操作,考虑使用deque直接
- 对内存敏感的场景,考虑指定vector作为stack的底层容器
4.3 常见错误与调试技巧
- 空容器访问:在调用top()或front()前务必检查empty()
cpp复制if(!s.empty()) {
auto val = s.top(); // 安全访问
}
-
迭代器失效:stack和queue不提供迭代器,这是设计使然
-
性能陷阱:频繁push/pop时,基于vector的stack可能导致内存重新分配
-
死锁排查:在多线程环境中使用queue时,确保适当的同步机制
调试技巧:
- 在复杂算法中使用stack/queue时,可以添加调试输出
- 对于自定义stack/queue,实现完整的异常安全保证
- 使用static_assert检查模板参数的有效性
5. 从stack和queue到deque的进阶
5.1 deque的双端特性
deque(双端队列)结合了stack和queue的特性,允许在两端高效地进行插入和删除操作:
cpp复制#include <deque>
#include <iostream>
int main() {
std::deque<int> dq;
dq.push_back(1); // 后端插入:1
dq.push_front(2); // 前端插入:2 1
dq.push_back(3); // 后端插入:2 1 3
std::cout << "Front: " << dq.front() << std::endl; // 2
std::cout << "Back: " << dq.back() << std::endl; // 3
dq.pop_front(); // 移除2
dq.pop_back(); // 移除3
std::cout << "Remaining: " << dq.front() << std::endl; // 1
return 0;
}
5.2 deque的内部实现原理
deque通常实现为一系列固定大小的数组块(称为缓冲区),通过一个中央映射结构管理这些块。这种设计使得:
- 随机访问性能接近vector(O(1))
- 首尾插入/删除性能接近list(O(1))
- 中间插入/删除性能较差(O(n))
5.3 何时选择deque而非stack/queue
- 需要同时使用LIFO和FIFO操作时
- 需要随机访问容器元素时(stack/queue不支持)
- 需要遍历容器内容时(stack/queue不提供迭代器)
- 对内存使用有特殊要求时(deque比list更紧凑)
6. 实际项目中的综合应用案例
6.1 使用stack实现表达式求值
下面是一个简单的表达式求值器,使用两个stack分别处理操作数和运算符:
cpp复制#include <stack>
#include <string>
#include <cctype>
#include <iostream>
int evaluateExpression(const std::string& expr) {
std::stack<int> operands;
std::stack<char> operators;
for(size_t i = 0; i < expr.size(); ++i) {
if(expr[i] == ' ') continue;
if(isdigit(expr[i])) {
int num = 0;
while(i < expr.size() && isdigit(expr[i])) {
num = num * 10 + (expr[i] - '0');
++i;
}
--i;
operands.push(num);
}
else if(expr[i] == '(') {
operators.push(expr[i]);
}
else if(expr[i] == ')') {
while(!operators.empty() && operators.top() != '(') {
// 执行运算...
}
operators.pop(); // 弹出'('
}
else {
// 处理运算符优先级...
}
}
// 处理剩余运算符...
return operands.top();
}
6.2 使用queue实现BFS算法
广度优先搜索的典型实现:
cpp复制#include <queue>
#include <vector>
#include <unordered_set>
void bfs(const std::vector<std::vector<int>>& graph, int start) {
std::queue<int> q;
std::unordered_set<int> visited;
q.push(start);
visited.insert(start);
while(!q.empty()) {
int current = q.front();
q.pop();
// 处理当前节点...
std::cout << "Visiting: " << current << std::endl;
for(int neighbor : graph[current]) {
if(visited.find(neighbor) == visited.end()) {
visited.insert(neighbor);
q.push(neighbor);
}
}
}
}
6.3 性能优化实践
在性能关键的应用中,可以考虑以下优化:
- 预分配内存:对于已知最大大小的stack/queue,可以先reserve空间
cpp复制std::stack<int, std::vector<int>> s;
s.c.reserve(1000); // 假设我们知道最多需要1000个元素
- 使用自定义分配器:针对特定内存模式优化
cpp复制#include <memory_resource>
std::pmr::monotonic_buffer_resource pool;
std::pmr::polymorphic_allocator<int> alloc(&pool);
std::pmr::deque<int> pmr_deque(alloc);
std::stack<int, std::pmr::deque<int>> pmr_stack(pmr_deque);
- 批量操作:减少频繁调用push/pop的开销
7. 现代C++中的新特性与最佳实践
7.1 使用emplace替代push
现代C++推荐使用emplace直接在容器中构造对象,避免不必要的拷贝:
cpp复制std::stack<std::pair<int, std::string>> s;
s.emplace(42, "answer"); // 直接在栈上构造pair
7.2 移动语义的应用
对于大型对象,利用移动语义提高性能:
cpp复制std::string largeData = getLargeData();
std::stack<std::string> s;
s.push(std::move(largeData)); // 移动而非拷贝
7.3 异常安全保证
编写异常安全的stack/queue操作:
cpp复制template<typename T>
void safePush(std::stack<T>& s, const T& value) {
std::stack<T> temp(s);
temp.push(value);
std::swap(s, temp); // 如果push抛出异常,原stack不受影响
}
7.4 C++20/23中的新特性
- 范围适配器:C++23可能引入stack/queue的范围构造
- 格式化输出:C++20的format库方便调试输出
cpp复制#include <format>
std::cout << std::format("Stack size: {}", s.size());
8. 从stack/queue到更高级的数据结构
掌握了stack和queue后,可以进一步学习:
- 优先队列(priority_queue):基于堆实现的队列,元素按优先级出队
- 单调栈:解决特定类型的问题,如寻找下一个更大元素
- 并发队列:多线程环境下的线程安全队列实现
- 环形缓冲区:固定大小的高效队列实现
例如,单调栈解决"每日温度"问题:
cpp复制std::vector<int> dailyTemperatures(const std::vector<int>& temps) {
std::vector<int> result(temps.size(), 0);
std::stack<int> s; // 存储索引
for(int i = 0; i < temps.size(); ++i) {
while(!s.empty() && temps[i] > temps[s.top()]) {
int prev = s.top();
s.pop();
result[prev] = i - prev;
}
s.push(i);
}
return result;
}
在实际项目中,我经常发现stack和queue的正确使用能极大简化算法设计。特别是在处理具有特定顺序要求的数据时,它们提供的抽象能让我们专注于业务逻辑而非底层实现细节。一个实用的建议是:当你的问题涉及"撤销"或"回退"时,考虑stack;当涉及"排队"或"顺序处理"时,考虑queue。
