1. C++中的stack与queue:从入门到实战
作为C++标准模板库(STL)中最基础也最常用的两种数据结构,stack(栈)和queue(队列)几乎出现在所有需要处理数据序列的场景中。我在实际开发中发现,很多初学者虽然能写出简单的栈和队列操作,但对它们的底层实现原理、性能特点以及实际应用场景的理解往往不够深入。这篇文章将带你系统掌握这两种数据结构,包括它们的标准库用法、典型应用场景、底层实现原理以及常见问题解决方案。
2. stack深度解析与应用实践
2.1 stack的核心特性与标准库接口
stack是一种后进先出(LIFO)的数据结构,可以想象成一摞盘子——你只能从最上面放入或取出盘子。在C++中,stack被实现为一个容器适配器,这意味着它基于其他底层容器(如deque或list)构建,但提供了更简单的接口。
标准库提供的stack主要接口包括:
- push(val): 将元素压入栈顶
- pop(): 移除栈顶元素(注意不返回该元素)
- top(): 访问栈顶元素(不移除)
- empty(): 检查栈是否为空
- size(): 获取栈中元素数量
重要提示:stack的pop()操作只移除元素不返回它,这是为了异常安全考虑。必须先通过top()获取元素,再调用pop()移除。
2.2 stack的典型应用场景与实战案例
2.2.1 最小栈实现(LeetCode 155)
最小栈要求在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(); }
};
2.2.2 逆波兰表达式求值(LeetCode 150)
逆波兰表达式(后缀表达式)的计算是stack的经典应用:
cpp复制int evalRPN(vector<string>& tokens) {
stack<int> s;
for(const auto& token : tokens) {
if(token == "+" || token == "-" || token == "*" || token == "/") {
int b = s.top(); s.pop();
int a = s.top(); s.pop();
if(token == "+") s.push(a + b);
else if(token == "-") s.push(a - b);
else if(token == "*") s.push(a * b);
else s.push(a / b);
} else {
s.push(stoi(token));
}
}
return s.top();
}
2.3 stack的底层实现原理
虽然标准库默认使用deque作为stack的底层容器,但我们也可以用vector或list自行实现:
cpp复制template<typename T, typename Container = std::deque<T>>
class Stack {
private:
Container c;
public:
void push(const T& value) { c.push_back(value); }
void pop() { c.pop_back(); }
T& top() { return c.back(); }
const T& top() const { return c.back(); }
bool empty() const { return c.empty(); }
size_t size() const { return c.size(); }
};
选择不同底层容器会影响性能:
- vector:内存连续,随机访问快,但扩容时可能需要重新分配内存
- list:插入删除稳定,但内存不连续,缓存不友好
- deque(默认):折中方案,支持快速两端操作
3. queue全面剖析与高级用法
3.1 queue的基本特性与标准接口
queue是先进先出(FIFO)的数据结构,就像排队一样,先来的人先得到服务。标准库提供的queue接口包括:
- push(val): 在队尾插入元素
- pop(): 移除队首元素
- front(): 访问队首元素
- back(): 访问队尾元素
- empty(): 检查是否为空
- size(): 获取元素数量
3.2 queue的典型应用与实战
3.2.1 用队列实现栈(LeetCode 225)
虽然看起来反直觉,但可以用两个队列实现栈的功能:
cpp复制class MyStack {
private:
queue<int> q1, q2;
public:
void push(int x) {
q1.push(x);
}
int pop() {
while(q1.size() > 1) {
q2.push(q1.front());
q1.pop();
}
int val = q1.front();
q1.pop();
swap(q1, q2);
return val;
}
int top() {
return q1.back();
}
bool empty() {
return q1.empty();
}
};
3.2.2 二叉树的层序遍历
queue非常适合处理层级遍历问题:
cpp复制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;
}
3.3 queue的底层实现与性能考量
标准库queue默认使用deque作为底层容器,但也可以用list实现:
cpp复制template<typename T, typename Container = std::deque<T>>
class Queue {
private:
Container c;
public:
void push(const T& value) { c.push_back(value); }
void pop() { c.pop_front(); }
T& front() { return c.front(); }
const T& front() const { return c.front(); }
T& back() { return c.back(); }
const T& back() const { return c.back(); }
bool empty() const { return c.empty(); }
size_t size() const { return c.size(); }
};
选择底层容器时考虑:
- list:稳定的O(1)插入删除,但内存不连续
- deque:内存部分连续,两端操作高效,适合大多数场景
4. priority_queue深入解析
4.1 优先级队列的本质与特性
priority_queue不是严格的FIFO结构,而是按照优先级出队,底层通常用堆(heap)实现。默认情况下是最大堆,即优先级高的元素先出队。
关键特性:
- 插入操作O(log n)
- 获取/删除最大元素O(log n)
- 查找最大元素O(1)
4.2 priority_queue的典型应用
4.2.1 查找第K大元素(LeetCode 215)
cpp复制int findKthLargest(vector<int>& nums, int k) {
priority_queue<int> pq(nums.begin(), nums.end());
for(int i = 0; i < k-1; ++i) {
pq.pop();
}
return pq.top();
}
更高效的解法是使用最小堆并维护大小为k的堆:
cpp复制int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> pq;
for(int num : nums) {
pq.push(num);
if(pq.size() > k) {
pq.pop();
}
}
return pq.top();
}
4.2.2 合并K个有序链表(LeetCode 23)
cpp复制struct Compare {
bool operator()(ListNode* a, ListNode* b) {
return a->val > b->val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
priority_queue<ListNode*, vector<ListNode*>, Compare> pq;
for(auto list : lists) {
if(list) pq.push(list);
}
ListNode dummy(0);
ListNode* tail = &dummy;
while(!pq.empty()) {
ListNode* node = pq.top();
pq.pop();
tail->next = node;
tail = tail->next;
if(node->next) {
pq.push(node->next);
}
}
return dummy.next;
}
4.3 自定义优先级比较
priority_queue允许自定义比较函数,这对于复杂数据类型特别有用:
cpp复制class Task {
public:
int priority;
string description;
bool operator<(const Task& other) const {
return priority < other.priority; // 默认最大堆
}
};
// 使用示例
priority_queue<Task> taskQueue;
taskQueue.push({3, "Low priority task"});
taskQueue.push({8, "High priority task"});
或者使用函数对象自定义比较:
cpp复制auto cmp = [](const Task& a, const Task& b) {
return a.priority < b.priority;
};
priority_queue<Task, vector<Task>, decltype(cmp)> customQueue(cmp);
5. 容器适配器深度解析
5.1 适配器设计模式
容器适配器(Container Adapter)是一种设计模式,它通过封装已有的容器接口,提供新的、更特定的功能接口。stack、queue和priority_queue都是容器适配器,它们基于其他序列容器(如deque、vector)构建,但提供了更受限、更专用的接口。
5.2 deque的底层机制
deque(double-ended queue)是stack和queue默认的底层容器,它结合了vector和list的优点:
- 支持随机访问(类似vector)
- 支持高效的两端插入删除(类似list)
- 由多个固定大小的数组块组成,通过中控器(map)管理
deque的内存布局:
code复制中控器 -> [数组块1][数组块2][数组块3]...
每个数组块大小固定(如512字节),当需要扩容时只需添加新的数组块,不需要像vector那样重新分配和拷贝所有元素。
5.3 为什么选择deque作为默认底层容器
stack和queue选择deque作为默认底层容器有几个关键原因:
- 两端操作高效:deque的push_back/pop_back和push_front/pop_front都是O(1)
- 内存效率高:不像list需要为每个元素存储额外指针
- 缓存友好:数据局部性比list更好
- 扩容成本低:不需要像vector那样全量拷贝
5.4 性能对比与容器选择
不同底层容器的性能特点:
| 操作 | vector | deque | list |
|---|---|---|---|
| push_front | O(n) | O(1) | O(1) |
| push_back | O(1)* | O(1) | O(1) |
| pop_front | O(n) | O(1) | O(1) |
| pop_back | O(1) | O(1) | O(1) |
| 随机访问 | O(1) | O(1) | O(n) |
| 内存连续性 | 是 | 部分 | 否 |
*vector的push_back平均O(1),最坏情况O(n)需要扩容
选择建议:
- 需要随机访问:vector或deque
- 频繁两端操作:deque
- 大量中间插入删除:list
- 仅需栈功能:stack with deque(默认)
- 仅需队列功能:queue with deque(默认)
6. 实际开发中的经验与陷阱
6.1 常见错误与避免方法
-
直接使用pop()的返回值
cpp复制int val = s.pop(); // 错误!pop()不返回值 // 正确做法 int val = s.top(); s.pop(); -
在empty()时调用top()或pop()
这会导致未定义行为,必须先检查:cpp复制if(!s.empty()) { auto val = s.top(); s.pop(); } -
忽略priority_queue的默认最大堆性质
需要最小堆时要显式指定比较器:cpp复制priority_queue<int, vector<int>, greater<int>> minHeap;
6.2 性能优化技巧
-
预先分配空间
对于vector作为底层容器的情况:cpp复制stack<int, vector<int>> s; s.c.reserve(1000); // 需要访问底层容器,可能不总是可行 -
选择合适的底层容器
- 如果主要做栈操作,vector可能比deque稍快
- 如果同时需要队列功能,坚持使用默认deque
-
批量操作优化
对于多次连续操作,有时直接操作底层容器更高效(但破坏了封装):cpp复制// 不推荐但有时有效 vector<int>& underlying = s.c; underlying.insert(underlying.end(), newElements.begin(), newElements.end());
6.3 线程安全考虑
标准库容器适配器不是线程安全的。在多线程环境中使用时需要同步:
cpp复制mutex mtx;
stack<int> s;
// 线程1
{
lock_guard<mutex> lock(mtx);
s.push(42);
}
// 线程2
{
lock_guard<mutex> lock(mtx);
if(!s.empty()) {
int val = s.top();
s.pop();
}
}
6.4 自定义内存管理
对于性能关键的应用,可以考虑自定义分配器:
cpp复制template<typename T>
class MyAllocator {
// 自定义内存分配实现
};
stack<int, deque<int, MyAllocator<int>>> customStack;
7. 高级应用与扩展思考
7.1 单调栈技术
单调栈是一种特殊的栈结构,用于高效解决"下一个更大元素"类问题:
cpp复制vector<int> nextGreaterElement(vector<int>& nums) {
vector<int> result(nums.size(), -1);
stack<int> s; // 存储索引
for(int i = 0; i < nums.size(); ++i) {
while(!s.empty() && nums[s.top()] < nums[i]) {
result[s.top()] = nums[i];
s.pop();
}
s.push(i);
}
return result;
}
7.2 双端队列解决滑动窗口问题
deque可以用来高效解决滑动窗口最大值问题:
cpp复制vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq;
vector<int> result;
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) {
result.push_back(nums[dq.front()]);
}
}
return result;
}
7.3 基于堆的调度系统设计
priority_queue非常适合实现任务调度系统:
cpp复制class Scheduler {
private:
struct Task {
time_t execTime;
function<void()> job;
bool operator<(const Task& other) const {
return execTime > other.execTime; // 最小堆
}
};
priority_queue<Task> tasks;
mutex mtx;
condition_variable cv;
atomic<bool> running{true};
thread worker;
void run() {
while(running) {
unique_lock<mutex> lock(mtx);
if(tasks.empty()) {
cv.wait(lock);
continue;
}
auto nextTask = tasks.top();
if(nextTask.execTime <= time(nullptr)) {
tasks.pop();
lock.unlock();
nextTask.job();
} else {
cv.wait_until(lock, chrono::system_clock::from_time_t(nextTask.execTime));
}
}
}
public:
Scheduler() : worker(&Scheduler::run, this) {}
~Scheduler() {
running = false;
cv.notify_all();
worker.join();
}
void schedule(time_t when, function<void()> what) {
lock_guard<mutex> lock(mtx);
tasks.push({when, what});
cv.notify_one();
}
};
8. 现代C++中的改进与最佳实践
8.1 使用emplace替代push
C++11引入了emplace操作,可以直接在容器中构造元素,避免临时对象:
cpp复制stack<pair<int, string>> s;
s.push(make_pair(1, "hello")); // 传统方式
s.emplace(1, "hello"); // 更高效,直接构造
8.2 移动语义支持
现代C++支持移动语义,可以高效转移资源:
cpp复制stack<vector<int>> s;
vector<int> largeVec(1000000);
s.push(move(largeVec)); // 移动而非拷贝
8.3 结构化绑定(C++17)
处理栈顶元素时更简洁:
cpp复制stack<pair<int, string>> s;
s.emplace(1, "test");
auto [num, str] = s.top(); // 结构化绑定
8.4 使用非标准但实用的扩展
一些库提供了额外功能,如boost::stack的线程安全版本:
cpp复制#include <boost/thread/sync_stack.hpp>
boost::sync_stack<int> threadSafeStack;
在实际项目中,理解stack和queue的底层原理和特性,能够帮助开发者做出更合理的设计选择,编写出更高效、更健壮的代码。从简单的算法题到复杂的系统设计,这两种基础数据结构都扮演着重要角色。