1. 为什么需要掌握Stack与Queue?
在C++开发中,Stack(栈)和Queue(队列)是两种最基础也最重要的数据结构。它们看似简单,但却是构建复杂系统的基石。我见过太多开发者因为对这些基础数据结构理解不深入,导致在系统设计、算法实现时走了弯路。
Stack遵循LIFO(后进先出)原则,就像餐厅里叠放的盘子,你总是取最上面的那个。Queue则遵循FIFO(先进先出)原则,如同排队买票,先来的人先得到服务。这两种结构在计算机科学中无处不在:函数调用栈、表达式求值、BFS/DFS算法、消息队列系统...
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Stack完全指南
2.1 基础操作详解
C++标准库中的std::stack是一个容器适配器,默认基于deque实现。以下是核心操作:
cpp复制#include <stack>
using namespace std;
stack<int> s; // 声明一个int类型的栈
// 压栈操作
s.push(1); // 栈:[1]
s.push(2); // 栈:[1,2]
// 访问栈顶
int top = s.top(); // 返回2但不移除
// 出栈操作
s.pop(); // 移除栈顶元素,栈:[1]
// 实用方法
bool empty = s.empty(); // 判断是否为空
size_t size = s.size(); // 获取元素数量
注意:调用top()或pop()前必须检查empty(),否则可能导致未定义行为
2.2 经典应用场景
场景1:括号匹配检查
这是栈的经典用例,编译器常用此方法检查代码语法:
cpp复制bool isValidParentheses(const string& s) {
stack<char> st;
for (char c : s) {
if (c == '(' || c == '[' || c == '{') {
st.push(c);
} else {
if (st.empty()) return false;
char top = st.top();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
st.pop();
}
}
return st.empty();
}
场景2:表达式求值
栈可以高效处理中缀表达式转后缀表达式,以及后缀表达式求值:
cpp复制// 中缀转后缀示例
string infixToPostfix(const string& infix) {
stack<char> opStack;
string postfix;
// ...实现转换逻辑
return postfix;
}
2.3 性能优化技巧
-
容器选择:默认使用deque,但可以指定其他底层容器:
cpp复制stack<int, vector<int>> s; // 使用vector作为底层- vector:连续内存,适合频繁随机访问(但栈不需要)
- deque:默认选择,两端操作高效
- list:元素分散存储,适合大对象
-
内存预分配:如果知道栈的大致大小,可以预先reserve:
cpp复制vector<int> v; v.reserve(100); stack<int, vector<int>> s(v);
3. Queue完全指南
3.1 基础操作精讲
std::queue同样是一个容器适配器,默认基于deque:
cpp复制#include <queue>
using namespace std;
queue<int> q; // 声明int类型队列
// 入队操作
q.push(1); // 队列:[1]
q.push(2); // 队列:[1,2]
// 访问队首/队尾
int front = q.front(); // 返回1
int back = q.back(); // 返回2
// 出队操作
q.pop(); // 移除队首,队列:[2]
// 实用方法
bool empty = q.empty(); // 判断是否为空
size_t size = q.size(); // 获取元素数量
3.2 典型应用场景
场景1:BFS算法实现
队列是广度优先搜索的核心数据结构:
cpp复制void BFS(Node* root) {
if (!root) return;
queue<Node*> q;
q.push(root);
while (!q.empty()) {
Node* current = q.front();
q.pop();
// 处理当前节点...
// 将子节点入队
for (Node* child : current->children) {
if (child) q.push(child);
}
}
}
场景2:消息缓冲系统
队列天然适合生产者-消费者模型:
cpp复制class MessageQueue {
queue<string> messages;
mutex mtx;
public:
void push(const string& msg) {
lock_guard<mutex> lock(mtx);
messages.push(msg);
}
string pop() {
lock_guard<mutex> lock(mtx);
if (messages.empty()) return "";
string msg = messages.front();
messages.pop();
return msg;
}
};
3.3 高级变种:Priority Queue
优先队列(堆)是队列的重要变体:
cpp复制#include <queue>
priority_queue<int> pq; // 默认大顶堆
pq.push(3); // [3]
pq.push(1); // [3,1]
pq.push(4); // [4,3,1]
int top = pq.top(); // 4
pq.pop(); // [3,1]
自定义比较函数示例:
cpp复制struct Compare {
bool operator()(const pair<int,int>& a, const pair<int,int>& b) {
return a.second > b.second; // 小顶堆
}
};
priority_queue<pair<int,int>, vector<pair<int,int>>, Compare> pq;
4. 实战习题精解
4.1 最小栈问题
设计一个能在O(1)时间内获取最小值的栈:
cpp复制class MinStack {
stack<int> mainStack;
stack<int> minStack;
public:
void push(int x) {
mainStack.push(x);
if (minStack.empty() || x <= minStack.top()) {
minStack.push(x);
}
}
void pop() {
if (mainStack.top() == minStack.top()) {
minStack.pop();
}
mainStack.pop();
}
int top() { return mainStack.top(); }
int getMin() { return minStack.top(); }
};
4.2 用队列实现栈
使用队列模拟栈的行为:
cpp复制class MyStack {
queue<int> q;
public:
void push(int x) {
q.push(x);
for (int i = 1; i < q.size(); ++i) {
q.push(q.front());
q.pop();
}
}
int pop() {
int val = q.front();
q.pop();
return val;
}
int top() { return q.front(); }
bool empty() { return q.empty(); }
};
4.3 滑动窗口最大值
使用双端队列高效解决:
cpp复制vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq;
vector<int> res;
for (int i = 0; i < nums.size(); ++i) {
// 移除超出窗口的元素
if (!dq.empty() && dq.front() == i - k) {
dq.pop_front();
}
// 移除小于当前元素的索引
while (!dq.empty() && nums[dq.back()] < nums[i]) {
dq.pop_back();
}
dq.push_back(i);
// 窗口形成后记录最大值
if (i >= k - 1) {
res.push_back(nums[dq.front()]);
}
}
return res;
}
5. 性能对比与选择指南
5.1 时间复杂度对比
| 操作 | Stack | Queue | Priority Queue |
|---|---|---|---|
| 插入 | O(1) | O(1) | O(log n) |
| 删除 | O(1) | O(1) | O(log n) |
| 访问顶部 | O(1) | O(1) | O(1) |
| 搜索 | O(n) | O(n) | O(n) |
5.2 容器选择建议
- 默认情况:使用标准库提供的stack和queue,它们已经针对通用场景优化
- 内存敏感:考虑使用vector作为stack的底层容器
- 并发环境:需要自行添加锁机制或使用线程安全队列
- 特殊需求:
- 需要随机访问:考虑deque
- 元素非常大:考虑list
- 需要优先级:使用priority_queue
6. 常见陷阱与调试技巧
6.1 典型错误案例
-
空容器访问:
cpp复制stack<int> s; s.pop(); // 崩溃! -
迭代器失效:
cpp复制queue<vector<int>> q; q.push({1,2,3}); auto& vec = q.front(); q.pop(); // vec现在悬空引用! -
优先级队列比较函数:
cpp复制// 错误:应该是operator()而不是operator< struct Compare { bool operator<(const T& a, const T& b) { ... } // 错误 };
6.2 调试建议
- 边界检查:所有pop/front/top操作前检查empty()
- 打印调试:临时添加打印语句查看容器状态
cpp复制void debugPrint(stack<int> s) { // 传值拷贝 while (!s.empty()) { cout << s.top() << " "; s.pop(); } cout << endl; } - 使用RAII:对于需要加锁的场景,使用lock_guard确保异常安全
7. 扩展学习路径
-
进阶数据结构:
- 双端队列(deque)
- 单调栈/队列
- 阻塞队列
-
相关算法:
- 栈:递归转非递归、回溯算法
- 队列:拓扑排序、Dijkstra算法
- 优先队列:A*算法、Huffman编码
-
系统设计应用:
- 浏览器历史记录(栈)
- 任务调度系统(队列)
- 事件处理系统(优先队列)
在实际项目中,我经常发现这些基础数据结构的巧妙运用能极大简化代码逻辑。比如用单调栈解决Next Greater Element问题,或者用优先队列实现高效的定时任务系统。掌握它们的核心在于理解其行为特性而非死记API。
