1. 理解栈与队列的基本概念
在C++编程中,栈(Stack)和队列(Queue)是两种最基础也最重要的数据结构。它们都属于线性数据结构,但在数据存取方式上有着本质区别。
栈遵循LIFO(Last In First Out)原则,就像我们日常生活中叠放的盘子——最后放上去的盘子总是最先被取用。这种特性使得栈在函数调用、表达式求值、括号匹配等场景中表现出色。C++标准库中的std::stack就是基于这种模型实现的容器适配器。
队列则遵循FIFO(First In First Out)原则,类似于排队买票的队伍——先来的人先得到服务。这种特性让队列在任务调度、消息传递、广度优先搜索等场景中非常有用。C++提供了std::queue作为队列的标准实现。
提示:虽然栈和队列都可以用数组或链表实现,但在C++中更推荐使用标准库提供的容器适配器,它们已经针对性能做了优化,并且提供了类型安全的接口。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C++中栈的实现与使用
2.1 std::stack的基本操作
C++标准库中的std::stack是一个容器适配器,它基于其他容器(默认是std::deque)实现栈的功能。以下是它的核心接口:
cpp复制#include <stack>
std::stack<int> myStack;
// 压栈操作
myStack.push(10); // 栈: [10]
myStack.push(20); // 栈: [10, 20]
// 访问栈顶元素
int topElement = myStack.top(); // 返回20
// 弹出栈顶元素
myStack.pop(); // 栈: [10]
// 检查栈是否为空
bool isEmpty = myStack.empty(); // 返回false
// 获取栈的大小
size_t stackSize = myStack.size(); // 返回1
2.2 栈的底层容器选择
std::stack默认使用std::deque作为底层容器,但我们可以根据需要指定其他容器:
cpp复制#include <vector>
#include <list>
#include <stack>
// 使用vector作为底层容器
std::stack<int, std::vector<int>> vectorStack;
// 使用list作为底层容器
std::stack<int, std::list<int>> listStack;
选择不同底层容器会影响性能特征:
std::vector:内存连续,缓存友好,但增长时需要重新分配内存std::deque(默认):分段连续,增长时不需要整体重新分配std::list:每个操作都是O(1),但缓存不友好
2.3 栈的典型应用场景
- 函数调用栈:编译器使用栈来管理函数调用和返回地址
- 表达式求值:中缀表达式转后缀表达式并求值
- 括号匹配:检查代码中的括号是否成对出现
- 撤销操作:许多编辑器使用栈实现撤销(Undo)功能
cpp复制// 括号匹配示例
bool isBalanced(const std::string& expr) {
std::stack<char> s;
for (char c : expr) {
if (c == '(' || c == '[' || c == '{') {
s.push(c);
} else {
if (s.empty()) return false;
char top = s.top();
s.pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
}
}
return s.empty();
}
3. C++中队列的实现与使用
3.1 std::queue的基本操作
std::queue是C++标准库提供的队列实现,默认也基于std::deque。它的核心接口如下:
cpp复制#include <queue>
std::queue<int> myQueue;
// 入队操作
myQueue.push(10); // 队列: [10]
myQueue.push(20); // 队列: [10, 20]
// 访问队首元素
int frontElement = myQueue.front(); // 返回10
// 访问队尾元素
int backElement = myQueue.back(); // 返回20
// 出队操作
myQueue.pop(); // 队列: [20]
// 检查队列是否为空
bool isEmpty = myQueue.empty(); // 返回false
// 获取队列大小
size_t queueSize = myQueue.size(); // 返回1
3.2 队列的底层容器选择
与栈类似,队列也可以指定不同的底层容器:
cpp复制#include <list>
#include <queue>
// 使用list作为底层容器
std::queue<int, std::list<int>> listQueue;
需要注意的是,std::vector不能直接用作std::queue的底层容器,因为vector没有提供pop_front()操作。如果需要使用vector,可以考虑std::deque。
3.3 队列的典型应用场景
- 任务调度:操作系统使用队列管理进程调度
- 消息传递:生产者-消费者模型中传递消息
- 广度优先搜索(BFS):图算法中按层次遍历节点
- 打印队列:管理等待打印的文档
cpp复制// 使用队列实现BFS示例
void BFS(const std::vector<std::vector<int>>& graph, int start) {
std::vector<bool> visited(graph.size(), false);
std::queue<int> q;
visited[start] = true;
q.push(start);
while (!q.empty()) {
int current = q.front();
q.pop();
std::cout << "Visiting: " << current << std::endl;
for (int neighbor : graph[current]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
q.push(neighbor);
}
}
}
}
4. 栈与队列的高级应用与性能考量
4.1 双栈实现队列
一个有趣的面试题是如何用两个栈实现队列。这种实现虽然不如原生队列高效,但展示了数据结构的灵活性:
cpp复制class QueueUsingStacks {
private:
std::stack<int> input, output;
void transfer() {
while (!input.empty()) {
output.push(input.top());
input.pop();
}
}
public:
void push(int x) {
input.push(x);
}
int pop() {
if (output.empty()) {
transfer();
}
int x = output.top();
output.pop();
return x;
}
int peek() {
if (output.empty()) {
transfer();
}
return output.top();
}
bool empty() {
return input.empty() && output.empty();
}
};
这种实现的摊还时间复杂度为O(1),因为每个元素最多被压入和弹出每个栈各一次。
4.2 循环队列
循环队列是一种更高效的队列实现,它通过重用底层数组空间来避免数据搬移:
cpp复制class CircularQueue {
private:
std::vector<int> data;
int head, tail;
int size;
public:
CircularQueue(int k) : data(k), head(0), tail(0), size(0) {}
bool enQueue(int value) {
if (isFull()) return false;
data[tail] = value;
tail = (tail + 1) % data.size();
size++;
return true;
}
bool deQueue() {
if (isEmpty()) return false;
head = (head + 1) % data.size();
size--;
return true;
}
int Front() {
if (isEmpty()) return -1;
return data[head];
}
int Rear() {
if (isEmpty()) return -1;
return data[(tail - 1 + data.size()) % data.size()];
}
bool isEmpty() { return size == 0; }
bool isFull() { return size == data.size(); }
};
4.3 性能对比与选择建议
| 数据结构 | 插入复杂度 | 删除复杂度 | 访问复杂度 | 适用场景 |
|---|---|---|---|---|
| std::stack | O(1) | O(1) | O(1) | LIFO场景,函数调用等 |
| std::queue | O(1) | O(1) | O(1) | FIFO场景,任务调度等 |
| 双栈队列 | O(1)摊还 | O(1)摊还 | O(1)摊还 | 需要队列但只能使用栈的场合 |
| 循环队列 | O(1) | O(1) | O(1) | 固定大小队列,性能敏感场景 |
注意:在实际项目中,除非有特殊需求,否则应优先使用标准库提供的
std::stack和std::queue,它们已经过充分优化,并且提供了类型安全和异常安全的保证。
5. 常见问题与最佳实践
5.1 栈溢出问题
递归函数调用过深会导致栈溢出。例如:
cpp复制void infiniteRecursion() {
infiniteRecursion(); // 这将最终导致栈溢出
}
解决方案:
- 将递归改为迭代
- 增加栈大小(编译器选项)
- 使用堆分配的内存实现自定义栈
5.2 线程安全考虑
标准库的栈和队列不是线程安全的。在多线程环境中使用时需要额外的同步机制:
cpp复制#include <mutex>
template<typename T>
class ThreadSafeQueue {
private:
std::queue<T> queue;
mutable std::mutex mtx;
public:
void push(T value) {
std::lock_guard<std::mutex> lock(mtx);
queue.push(std::move(value));
}
bool try_pop(T& value) {
std::lock_guard<std::mutex> lock(mtx);
if (queue.empty()) return false;
value = std::move(queue.front());
queue.pop();
return true;
}
bool empty() const {
std::lock_guard<std::mutex> lock(mtx);
return queue.empty();
}
};
5.3 自定义分配器
对于性能关键的应用,可以考虑使用自定义分配器来优化内存使用:
cpp复制#include <memory_resource>
void customAllocatorExample() {
char buffer[1024];
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
std::pmr::polymorphic_allocator<int> alloc{&pool};
std::pmr::stack<int> customStack{alloc};
for (int i = 0; i < 100; ++i) {
customStack.push(i);
}
}
5.4 异常安全保证
标准库的栈和队列提供基本的异常安全保证:
push操作提供强异常安全保证(要么成功,要么保持原状)pop操作通常不返回被移除的元素,以避免异常风险
在实现自定义栈/队列时,也应当遵循这些原则。
