1. 栈与队列:程序世界的两大基石
在计算机科学的世界里,栈和队列就像是一对性格迥异的双胞胎。它们看似简单,却构成了无数复杂系统和算法的底层支撑。作为一名C++开发者,深入理解这两种数据结构的工作原理和实现方式,是通往高级编程的必经之路。
记得我第一次在面试中被要求手写栈的实现时,因为忽略了边界条件检查而惨遭淘汰。这段经历让我明白,真正掌握一个数据结构,不仅要理解它的概念,更要能够从零开始构建它,并处理各种边界情况。本文将带你从理论到实践,彻底征服栈和队列。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 栈:后进先出的艺术
2.1 栈的核心特性
栈(Stack)遵循LIFO(Last In First Out)原则,就像我们日常生活中的一摞盘子:
code复制 ┌──────┐
TOP → │ 40 │ ← 最后放入,最先取出
├──────┤
│ 30 │
├──────┤
│ 20 │
├──────┤
BOTTOM │ 10 │ ← 最先放入,最后取出
└──────┘
栈的操作被严格限制在栈顶(top)进行,主要包括:
- push:元素入栈
- pop:栈顶元素出栈
- top/peek:查看栈顶元素
- empty:判断栈是否为空
- size:获取栈中元素数量
2.2 栈的典型应用场景
-
函数调用栈:每次函数调用时,系统会将返回地址、参数和局部变量压入调用栈。函数返回时再依次弹出。这也是递归函数的基础支撑。
-
括号匹配:编译器检查代码中的括号是否合法闭合。遇到左括号压栈,右括号时弹出栈顶元素检查是否匹配。
-
表达式求值:将中缀表达式转换为后缀表达式(逆波兰表示法)后,用栈可以高效计算表达式值。
-
撤销操作(Undo):文本编辑器中的撤销功能通常使用两个栈实现,一个记录操作,一个用于撤销。
2.3 C++实现栈的两种方式
2.3.1 基于动态数组的实现
数组实现栈的优势在于内存连续,访问效率高。但需要处理扩容问题:
cpp复制template <typename T>
class ArrayStack {
private:
T* data; // 动态数组
int topIdx; // 栈顶索引
int capacity; // 当前容量
void resize() {
capacity *= 2;
T* newData = new T[capacity];
for(int i=0; i<=topIdx; ++i)
newData[i] = std::move(data[i]); // 移动语义提升效率
delete[] data;
data = newData;
}
public:
ArrayStack(int initCap=10) : capacity(initCap), topIdx(-1) {
data = new T[capacity];
}
~ArrayStack() { delete[] data; }
void push(const T& val) {
if(topIdx == capacity-1) resize();
data[++topIdx] = val;
}
void pop() {
if(empty()) throw std::runtime_error("Stack underflow");
--topIdx;
}
T& top() {
if(empty()) throw std::runtime_error("Stack is empty");
return data[topIdx];
}
bool empty() const { return topIdx == -1; }
int size() const { return topIdx + 1; }
};
实现要点:
- 使用模板类支持泛型
- 动态扩容策略(通常容量翻倍)
- 使用移动语义提升性能
- 完善的异常处理
2.3.2 基于链表的实现
链表实现栈的优势在于无需预先分配空间,但每个操作需要处理指针:
cpp复制template <typename T>
class LinkedStack {
private:
struct Node {
T data;
Node* next;
Node(const T& val, Node* n=nullptr) : data(val), next(n) {}
};
Node* topNode;
int count;
public:
LinkedStack() : topNode(nullptr), count(0) {}
~LinkedStack() {
while(!empty()) pop();
}
void push(const T& val) {
topNode = new Node(val, topNode);
++count;
}
void pop() {
if(empty()) throw std::runtime_error("Stack underflow");
Node* temp = topNode;
topNode = topNode->next;
delete temp;
--count;
}
T& top() {
if(empty()) throw std::runtime_error("Stack is empty");
return topNode->data;
}
bool empty() const { return topNode == nullptr; }
int size() const { return count; }
};
性能对比:
- 数组实现:访问速度快,但扩容时可能带来性能抖动
- 链表实现:每次操作有额外内存开销,但无需扩容
2.4 STL中的stack容器
C++标准库提供了现成的stack适配器:
cpp复制#include <stack>
#include <iostream>
int main() {
std::stack<int> s;
s.push(1);
s.push(2);
s.push(3);
while(!s.empty()) {
std::cout << s.top() << " "; // 输出 3 2 1
s.pop();
}
return 0;
}
STL的stack默认基于deque实现,也可以指定底层容器:
cpp复制std::stack<int, std::vector<int>> vecStack; // 基于vector
std::stack<int, std::list<int>> listStack; // 基于list
3. 队列:先进先出的哲学
3.1 队列的核心特性
队列(Queue)遵循FIFO(First In First Out)原则,就像排队买票:
code复制 入队 → [ 10 | 20 | 30 | 40 ] → 出队
(rear) (front)
基本操作包括:
- enqueue/push:元素入队
- dequeue/pop:队首元素出队
- front:访问队首元素
- back:访问队尾元素
- empty:判断队列是否为空
- size:获取队列元素数量
3.2 队列的应用场景
-
BFS算法:图的广度优先搜索使用队列来管理待访问节点。
-
任务调度:操作系统使用就绪队列来管理等待CPU的进程。
-
消息队列:分布式系统中用于解耦生产者和消费者。
-
打印机队列:管理多个打印任务的执行顺序。
3.3 C++实现队列的两种方式
3.3.1 基于循环数组的实现
普通数组实现队列会有"假溢出"问题,循环队列通过模运算解决:
cpp复制template <typename T>
class CircularQueue {
private:
T* data;
int front, rear;
int count;
int capacity;
void resize() {
int newCap = capacity * 2;
T* newData = new T[newCap];
// 将循环队列展平到新数组
for(int i=0; i<count; ++i)
newData[i] = std::move(data[(front + i) % capacity]);
delete[] data;
data = newData;
front = 0;
rear = count;
capacity = newCap;
}
public:
CircularQueue(int initCap=10) : capacity(initCap), front(0), rear(0), count(0) {
data = new T[capacity];
}
~CircularQueue() { delete[] data; }
void push(const T& val) {
if(count == capacity) resize();
data[rear] = val;
rear = (rear + 1) % capacity;
++count;
}
void pop() {
if(empty()) throw std::runtime_error("Queue underflow");
front = (front + 1) % capacity;
--count;
}
T& getFront() {
if(empty()) throw std::runtime_error("Queue is empty");
return data[front];
}
T& getRear() {
if(empty()) throw std::runtime_error("Queue is empty");
return data[(rear - 1 + capacity) % capacity];
}
bool empty() const { return count == 0; }
int size() const { return count; }
};
关键点:
- 使用模运算实现循环
- 需要单独维护count变量
- 扩容时需要展平数据
3.3.2 基于链表的实现
链表实现队列无需考虑容量问题:
cpp复制template <typename T>
class LinkedQueue {
private:
struct Node {
T data;
Node* next;
Node(const T& val) : data(val), next(nullptr) {}
};
Node* frontNode;
Node* rearNode;
int count;
public:
LinkedQueue() : frontNode(nullptr), rearNode(nullptr), count(0) {}
~LinkedQueue() {
while(!empty()) pop();
}
void push(const T& val) {
Node* newNode = new Node(val);
if(rearNode) {
rearNode->next = newNode;
} else {
frontNode = newNode;
}
rearNode = newNode;
++count;
}
void pop() {
if(empty()) throw std::runtime_error("Queue underflow");
Node* temp = frontNode;
frontNode = frontNode->next;
if(!frontNode) rearNode = nullptr;
delete temp;
--count;
}
T& getFront() {
if(empty()) throw std::runtime_error("Queue is empty");
return frontNode->data;
}
T& getRear() {
if(empty()) throw std::runtime_error("Queue is empty");
return rearNode->data;
}
bool empty() const { return frontNode == nullptr; }
int size() const { return count; }
};
3.4 STL中的queue容器
标准库queue的使用示例:
cpp复制#include <queue>
#include <iostream>
int main() {
std::queue<int> q;
q.push(1);
q.push(2);
q.push(3);
while(!q.empty()) {
std::cout << q.front() << " "; // 输出 1 2 3
q.pop();
}
return 0;
}
STL queue默认基于deque实现,也可以指定底层容器:
cpp复制std::queue<int, std::list<int>> listQueue;
3.5 扩展队列类型
3.5.1 双端队列(deque)
双端队列支持两端的高效插入删除:
cpp复制#include <deque>
#include <iostream>
int main() {
std::deque<int> dq;
dq.push_front(1); // 头部插入
dq.push_back(2); // 尾部插入
dq.pop_front(); // 头部删除
dq.pop_back(); // 尾部删除
// 支持随机访问
std::cout << dq[0] << std::endl;
return 0;
}
3.5.2 优先队列(priority_queue)
优先队列基于堆实现,总是返回优先级最高的元素:
cpp复制#include <queue>
#include <iostream>
int main() {
// 默认大根堆
std::priority_queue<int> maxHeap;
// 小根堆
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
maxHeap.push(3);
maxHeap.push(1);
maxHeap.push(4);
while(!maxHeap.empty()) {
std::cout << maxHeap.top() << " "; // 输出 4 3 1
maxHeap.pop();
}
return 0;
}
4. 栈与队列的深度对比
4.1 基本特性对比
| 特性 | 栈(Stack) | 队列(Queue) |
|---|---|---|
| 操作原则 | LIFO(后进先出) | FIFO(先进先出) |
| 操作端 | 仅栈顶 | 队尾入,队首出 |
| 核心操作 | push, pop, top | push, pop, front |
| 空间复杂度 | O(n) | O(n) |
| 常见实现 | 数组/链表 | 循环数组/链表 |
| 典型应用 | 函数调用、括号匹配、撤销 | BFS、任务调度、缓冲 |
4.2 性能对比
| 操作 | 数组栈 | 链表栈 | 循环队列 | 链表队列 |
|---|---|---|---|---|
| push | O(1)均摊 | O(1) | O(1)均摊 | O(1) |
| pop | O(1) | O(1) | O(1) | O(1) |
| top/front | O(1) | O(1) | O(1) | O(1) |
| 空间开销 | 需预分配 | 每个节点额外指针 | 需预分配 | 每个节点额外指针 |
4.3 选择指南
-
选择栈的情况:
- 需要撤销操作(如编辑器)
- 递归算法转非递归
- 需要反转元素顺序
-
选择队列的情况:
- 需要保持原始顺序
- 广度优先遍历
- 缓冲区和消息处理
-
实现选择建议:
- 对于栈:大多数情况下STL stack足够
- 对于队列:需要高性能时考虑循环队列
- 不确定大小时:链表实现更灵活
5. 实战经验与陷阱规避
5.1 常见错误
-
栈溢出:
- 递归过深导致调用栈溢出
- 数组实现的栈未检查边界
-
队列假满:
- 非循环数组实现的队列"假溢出"
- 忘记重置头尾指针
-
内存泄漏:
- 链表实现中忘记delete节点
- 异常安全处理不足
5.2 调试技巧
-
栈调试:
- 在push/pop时打印栈状态
- 检查top()前确保栈非空
-
队列调试:
- 可视化头尾指针位置
- 检查循环队列的模运算是否正确
5.3 性能优化
-
批量操作:
- 对于数组实现,预留足够空间减少扩容
- 考虑批量push/pop操作
-
内存局部性:
- 数组实现比链表有更好的缓存命中率
- 对于性能敏感场景,优先考虑数组实现
-
移动语义:
- 在resize时使用std::move减少拷贝
6. 进阶应用场景
6.1 使用栈实现队列
用两个栈可以模拟队列:
cpp复制class StackQueue {
private:
std::stack<int> inStack;
std::stack<int> outStack;
void transfer() {
while(!inStack.empty()) {
outStack.push(inStack.top());
inStack.pop();
}
}
public:
void push(int x) {
inStack.push(x);
}
int pop() {
if(outStack.empty()) transfer();
int val = outStack.top();
outStack.pop();
return val;
}
int peek() {
if(outStack.empty()) transfer();
return outStack.top();
}
bool empty() {
return inStack.empty() && outStack.empty();
}
};
6.2 使用队列实现栈
用单个队列实现栈:
cpp复制class QueueStack {
private:
std::queue<int> q;
public:
void push(int x) {
q.push(x);
for(int i=0; i<q.size()-1; ++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();
}
};
6.3 单调栈应用
解决"下一个更大元素"问题:
cpp复制vector<int> nextGreaterElement(vector<int>& nums) {
vector<int> res(nums.size());
stack<int> s;
for(int i=nums.size()-1; i>=0; --i) {
while(!s.empty() && s.top() <= nums[i]) {
s.pop();
}
res[i] = s.empty() ? -1 : s.top();
s.push(nums[i]);
}
return res;
}
6.4 双端队列应用
滑动窗口最大值:
cpp复制vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
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[dq.back()] < nums[i])
dq.pop_back();
dq.push_back(i);
// 窗口形成后记录最大值
if(i >= k-1) res.push_back(nums[dq.front()]);
}
return res;
}
7. 从理论到实践的建议
-
理解原理:先彻底理解栈的LIFO和队列的FIFO原则,不要急于编码。
-
手写实现:至少亲手实现一次数组栈和链表队列,处理各种边界情况。
-
STL源码:阅读STL中stack和queue的适配器实现,学习工业级代码。
-
算法应用:通过实际算法题(如括号匹配、BFS)加深理解。
-
性能分析:对不同实现进行性能测试,理解时间/空间权衡。
-
扩展思考:尝试实现双端队列、优先队列等变种。
-
实际项目:在项目中寻找适用场景,如用栈实现撤销功能,用队列处理任务。
记住,数据结构的价值在于应用。当你遇到问题时,能够自然想到"这个问题适合用栈/队列解决",才是真正的掌握。
