1. 为什么需要stack和queue?
在C++编程中,stack(栈)和queue(队列)是两种最基础也最重要的数据结构容器。它们之所以被广泛使用,是因为它们完美模拟了现实世界中的两种常见行为模式。
想象一下你在餐厅洗碗的场景:洗好的碗碟总是叠放在一起,最后洗的碗会放在最上面,而下次取用时也是从最上面开始拿——这就是典型的LIFO(Last In First Out,后进先出)行为,也就是stack的工作方式。
再想象你在银行排队办理业务:先来的人先办理,后来的人排在队尾等候——这就是FIFO(First In First Out,先进先出)行为,正是queue的工作机制。
在C++标准库中,stack和queue被归类为容器适配器(container adaptors),这意味着它们是在其他序列容器(如deque或list)基础上提供的接口封装。这种设计既保证了特定场景下的高效操作,又避免了重复造轮子。
关键区别:stack只允许在一端(称为栈顶)进行插入和删除操作,而queue则是在一端(队尾)插入,在另一端(队头)删除。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. stack的深度解析与实战应用
2.1 stack的核心操作
C++中的stack模板类定义在
cpp复制#include <stack>
std::stack<int> myStack;
// 压栈操作
myStack.push(10);
myStack.push(20);
// 访问栈顶元素(不删除)
int topElement = myStack.top(); // 返回20
// 弹出栈顶元素
myStack.pop(); // 移除20
// 检查是否为空
bool isEmpty = myStack.empty();
// 获取元素数量
size_t size = myStack.size();
在实际项目中,stack常用于以下场景:
- 函数调用栈的实现
- 表达式求值和语法解析
- 撤销(Undo)操作的历史记录
- 深度优先搜索(DFS)算法
2.2 一个真实的括号匹配案例
让我们看一个leetcode第20题"有效的括号"的解决方案:
cpp复制bool isValid(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();
st.pop();
if((c == ')' && top != '(') ||
(c == '}' && top != '{') ||
(c == ']' && top != '[')) {
return false;
}
}
}
return st.empty();
}
这个实现完美展示了stack的LIFO特性:最后遇到的左括号必须最先匹配。算法时间复杂度O(n),空间复杂度O(n),是最优解。
2.3 stack的底层实现选择
虽然stack的接口简单,但它的底层容器选择会影响性能。默认使用deque实现,但我们可以显式指定:
cpp复制// 使用vector作为底层容器
std::stack<int, std::vector<int>> vecStack;
// 使用list作为底层容器
std::stack<int, std::list<int>> listStack;
选择依据:
- vector:连续内存,缓存友好,但扩容时可能性能抖动
- list:稳定O(1)时间操作,但内存不连续,缓存不友好
- deque(默认):折中方案,支持快速首尾操作
3. queue的全面剖析与使用技巧
3.1 queue的基本操作
queue定义在
cpp复制#include <queue>
std::queue<int> myQueue;
// 入队
myQueue.push(10);
myQueue.push(20);
// 访问队首元素
int frontElement = myQueue.front(); // 返回10
// 访问队尾元素
int backElement = myQueue.back(); // 返回20
// 出队
myQueue.pop(); // 移除10
// 容量查询
bool isEmpty = myQueue.empty();
size_t size = myQueue.size();
queue的典型应用场景包括:
- 消息队列系统
- 打印机任务调度
- 广度优先搜索(BFS)算法
- 多线程中的生产者-消费者模式
3.2 用queue实现BFS算法
以下是二叉树层序遍历的经典实现:
cpp复制struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
if(!root) return result;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()) {
int levelSize = q.size();
vector<int> currentLevel;
for(int i = 0; i < levelSize; ++i) {
TreeNode* node = q.front();
q.pop();
currentLevel.push_back(node->val);
if(node->left) q.push(node->left);
if(node->right) q.push(node->right);
}
result.push_back(currentLevel);
}
return result;
}
这个实现展示了queue在算法中的核心作用:保证节点按照层级顺序被处理,先进入队列的节点先被访问。
3.3 queue的底层容器选择
与stack类似,queue也支持指定底层容器:
cpp复制// 使用list作为底层容器
std::queue<int, std::list<int>> listQueue;
// 默认使用deque
std::queue<int> defaultQueue;
重要限制:queue不能使用vector作为底层容器,因为vector没有提供pop_front()方法,无法高效实现出队操作。
4. 高级应用与性能优化
4.1 用stack实现最小栈
设计一个能在O(1)时间内获取最小元素的栈,是面试常见题。以下是经典解法:
cpp复制class MinStack {
private:
stack<int> dataStack;
stack<int> minStack;
public:
void push(int x) {
dataStack.push(x);
if(minStack.empty() || x <= minStack.top()) {
minStack.push(x);
}
}
void pop() {
if(dataStack.top() == minStack.top()) {
minStack.pop();
}
dataStack.pop();
}
int top() {
return dataStack.top();
}
int getMin() {
return minStack.top();
}
};
这个设计使用了辅助栈来同步记录最小值,虽然增加了空间复杂度,但保证了所有操作仍然是O(1)时间复杂度。
4.2 用queue实现循环队列
循环队列可以有效利用数组空间,避免普通队列的"假溢出"问题:
cpp复制class CircularQueue {
private:
vector<int> data;
int head, tail;
int size;
public:
CircularQueue(int k) : data(k), head(-1), tail(-1), size(k) {}
bool enQueue(int value) {
if(isFull()) return false;
if(isEmpty()) head = 0;
tail = (tail + 1) % size;
data[tail] = value;
return true;
}
bool deQueue() {
if(isEmpty()) return false;
if(head == tail) {
head = tail = -1;
} else {
head = (head + 1) % size;
}
return true;
}
int Front() {
return isEmpty() ? -1 : data[head];
}
int Rear() {
return isEmpty() ? -1 : data[tail];
}
bool isEmpty() {
return head == -1;
}
bool isFull() {
return (tail + 1) % size == head;
}
};
4.3 性能对比与选择建议
在LeetCode等算法题中,stack和queue的性能表现如下:
| 操作 | stack时间复杂度 | queue时间复杂度 |
|---|---|---|
| 插入 | O(1) | O(1) |
| 删除 | O(1) | O(1) |
| 访问顶部 | O(1) | O(1) |
| 随机访问 | 不支持 | 不支持 |
选择建议:
- 需要后进先出逻辑时,无脑选择stack
- 需要先进先出逻辑时,选择queue
- 需要随机访问时,考虑vector或deque
- 在性能敏感场景,考虑预分配内存避免动态扩容
5. 常见陷阱与最佳实践
5.1 空容器访问问题
新手常犯的错误是直接访问空stack或queue的顶部元素:
cpp复制std::stack<int> s;
int val = s.top(); // 未定义行为!可能崩溃
正确的做法是先检查是否为空:
cpp复制if(!s.empty()) {
int val = s.top();
// 安全操作
}
5.2 迭代器失效问题
stack和queue不提供迭代器接口,这是设计使然。如果需要遍历,应该:
- 对于stack:只能通过不断pop()来遍历,但会破坏原stack
- 对于queue:标准库queue也不支持遍历,考虑改用deque
5.3 多线程安全问题
标准库的stack和queue不是线程安全的。在多线程环境下,需要额外同步:
cpp复制#include <mutex>
std::stack<int> sharedStack;
std::mutex stackMutex;
// 线程安全的push操作
void safePush(int value) {
std::lock_guard<std::mutex> lock(stackMutex);
sharedStack.push(value);
}
5.4 内存分配优化
对于已知最大大小的stack/queue,可以预分配内存:
cpp复制// 预分配stack内存
std::stack<int> s;
s.reserve(1000); // 错误!stack没有reserve方法
// 正确做法:通过底层容器预分配
std::vector<int> storage;
storage.reserve(1000);
std::stack<int, std::vector<int>> s(storage);
对于queue,由于默认使用deque,其内存增长策略已经比较高效,通常不需要特殊处理。
在实际工程中,stack和queue的选择和使用远不止这些基础内容。它们往往是更复杂系统的构建基石,理解它们的特性和限制,能帮助开发者写出更高效、更健壮的代码。
