在C++标准模板库(STL)中,stack、queue和priority_queue是三个高频使用的容器适配器。它们不同于vector、list这样的原生容器,而是通过封装底层容器来实现特定的数据结构行为。这种设计模式让开发者能够零成本地使用栈、队列和堆这些经典数据结构,而无需关心底层实现细节。
容器适配器的本质是一种接口转换器,它基于设计模式中的适配器模式。就像多功能料理机可以通过不同配件实现榨汁、绞肉等特定功能一样,容器适配器将通用容器的复杂接口转换为符合特定数据结构规则的简化接口。这种设计带来了几个显著优势:
提示:容器适配器不管理内存,完全依赖封装的底层容器进行存储。这种设计符合单一职责原则,使每个组件专注于自己的核心功能。
栈是一种严格遵循后进先出(LIFO)原则的线性数据结构,所有操作都只能在栈顶进行。这种特性使得栈成为处理嵌套结构(如函数调用、括号匹配)的理想选择。
STL中的stack默认使用deque作为底层容器,但也可以指定vector或list。选择deque的原因在于:
cpp复制// stack的类模板声明
template <class T, class Container = deque<T>>
class stack;
stack提供了一组精简的接口,完美体现了栈的LIFO特性:
| 方法 | 描述 | 时间复杂度 |
|---|---|---|
| empty() | 检查栈是否为空 | O(1) |
| size() | 返回栈中元素数量 | O(1) |
| top() | 返回栈顶元素引用 | O(1) |
| push(val) | 将元素压入栈顶 | O(1) |
| pop() | 移除栈顶元素 | O(1) |
典型应用示例:括号匹配
cpp复制bool isBalanced(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();
st.pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
}
}
return st.empty();
}
经验分享:在算法竞赛中,使用vector实现的stack有时比默认deque版本更快,因为vector的缓存局部性更好。但在元素数量变化大的场景,deque更稳定。
队列遵循先进先出(FIFO)原则,就像现实中的排队一样,最早进入的元素最先被处理。这种特性使queue成为实现广度优先搜索(BFS)、任务调度等算法的理想选择。
STL中的queue默认也使用deque作为底层容器,原因与stack类似:
cpp复制// queue的类模板声明
template <class T, class Container = deque<T>>
class queue;
queue提供了一组符合FIFO原则的接口:
| 方法 | 描述 | 时间复杂度 |
|---|---|---|
| empty() | 检查队列是否为空 | O(1) |
| size() | 返回队列元素数量 | O(1) |
| front() | 返回队首元素引用 | O(1) |
| back() | 返回队尾元素引用 | O(1) |
| push(val) | 在队尾插入元素 | O(1) |
| pop() | 移除队首元素 | O(1) |
典型应用示例:二叉树的层序遍历
cpp复制struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
// 构造函数...
};
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;
}
性能提示:在元素数量已知且固定的情况下,使用循环队列可以避免内存重新分配的开销,提高性能。
priority_queue不是严格的FIFO结构,而是按照元素优先级出队。它的底层实现通常是最大堆或最小堆,具有以下特性:
cpp复制// priority_queue的类模板声明
template <class T, class Container = vector<T>,
class Compare = less<typename Container::value_type>>
class priority_queue;
priority_queue的接口与stack类似,但行为完全不同:
| 方法 | 描述 | 时间复杂度 |
|---|---|---|
| empty() | 检查队列是否为空 | O(1) |
| size() | 返回队列元素数量 | O(1) |
| top() | 返回优先级最高的元素 | O(1) |
| push(val) | 插入元素并调整堆结构 | O(logN) |
| pop() | 删除最高优先级元素并调整堆结构 | O(logN) |
典型应用示例:Top K问题
cpp复制vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> freqMap;
for (int num : nums) {
freqMap[num]++;
}
// 小顶堆,频率小的在前
priority_queue<pair<int, int>, vector<pair<int, int>>,
greater<pair<int, int>>> pq;
for (auto& [num, freq] : freqMap) {
pq.push({freq, num});
if (pq.size() > k) {
pq.pop();
}
}
vector<int> result;
while (!pq.empty()) {
result.push_back(pq.top().second);
pq.pop();
}
return vector<int>(result.rbegin(), result.rend());
}
priority_queue的强大之处在于可以灵活定义优先级规则。以下是几种常见方式:
自定义仿函数示例
cpp复制struct Task {
int priority;
string description;
// 其他字段...
};
struct CompareTask {
bool operator()(const Task& t1, const Task& t2) const {
// 返回true表示t1优先级低于t2
return t1.priority < t2.priority; // 大顶堆
}
};
priority_queue<Task, vector<Task>, CompareTask> taskQueue;
Lambda表达式示例
cpp复制auto cmp = [](const Task& left, const Task& right) {
return left.priority < right.priority;
};
priority_queue<Task, vector<Task>, decltype(cmp)> taskQueue(cmp);
避坑指南:priority_queue的pop()操作只移除元素不返回值,必须先调用top()获取值再pop(),否则会导致数据丢失。
deque(双端队列)作为stack和queue的默认底层容器,具有以下优势:
priority_queue默认使用vector作为底层容器,主要因为:
| 场景 | 推荐容器 | 理由 |
|---|---|---|
| 元素数量变化大的stack | deque | 避免vector频繁扩容 |
| 固定大小的stack | vector | 缓存友好,性能高 |
| 频繁中间操作的queue | list | deque中间操作效率低 |
| 元素体积大的priority_queue | deque | 减少元素搬迁开销 |
| 性能关键的priority_queue | vector | 随机访问最快 |
cpp复制// 使用list作为queue的底层容器
queue<int, list<int>> listQueue;
// 使用vector作为stack的底层容器
stack<int, vector<int>> vecStack;
// 使用deque作为priority_queue的底层容器
priority_queue<int, deque<int>> dequePQ;
技术细节:自定义容器必须满足适配器的接口要求。例如queue需要支持push_back()、pop_front(),因此不能用vector作为queue的底层容器。
stack的应用:
queue的应用:
priority_queue的应用:
emplace使用示例
cpp复制struct LargeObject {
LargeObject(int a, double b, string c)
: a(a), b(b), c(c) {}
// 数据成员...
};
stack<LargeObject> s;
s.emplace(1, 2.0, "hello"); // 直接在容器内构造,避免拷贝
在实际项目中,我曾遇到一个性能问题:使用priority_queue处理大量数据时性能不佳。通过分析发现是频繁的push/pop导致内存频繁分配释放。解决方案是改用自定义的内存池分配器,性能提升了3倍以上。