跳到主要内容C++ STL 栈与队列详解:基础概念、应用及模拟实现 | 极客日志C++算法
C++ STL 栈与队列详解:基础概念、应用及模拟实现
综述由AI生成C++ STL 中的栈(Stack)、队列(Queue)和优先队列(Priority Queue)。涵盖基本概念、常用操作、最小栈实现、基于 vector/list/deque 的模拟实现以及容器适配器原理。重点讲解了 deque 作为底层容器的优势及堆的实现逻辑,帮助读者深入理解数据结构在实际开发中的应用。
栈溢出29 浏览 
C++ 栈与队列详解:基础与进阶应用
前言
栈与队列是常见的数据结构,常被用于不同的算法场景。在本文中,我们将详细探讨栈与队列的基本概念、实际应用及其模拟实现。栈和队列在日常开发中的重要性不言而喻,通过对这两种数据结构的深入理解,将助你更加灵活地应对算法题目和工程开发。
第一章:栈的介绍与使用
1.1 栈的介绍
栈 (Stack) 是一种 后进先出 (LIFO, Last In First Out) 的数据结构。这意味着栈中最后一个被插入的元素会第一个被移出。这种特性使得栈在实现某些算法时非常有用,例如函数调用栈、表达式求值以及括号匹配等。
栈提供的基本操作包括:
- push():将元素压入栈顶。
- pop():将栈顶元素弹出。
- top():获取栈顶元素。
- empty():检查栈是否为空。
- size():获取栈中元素的数量。
常见的栈使用场景有表达式求值、深度优先搜索(DFS)以及回溯法。
1.2 栈的使用
1.2.1 最小栈
最小栈 (Min Stack) 是栈的扩展应用,除了提供基本的栈操作外,还能够在常量时间内返回栈中的最小值。下面展示一个最小栈的具体实现。
#include <stack>
using namespace std;
class MinStack {
public:
MinStack() {}
void push(int x) {
_elem.push(x);
if (_min.empty() || x <= _min.top()) {
_min.push(x);
}
}
{
(_min.() == _elem.()) {
_min.();
}
_elem.();
}
{
_elem.();
}
{
_min.();
}
:
stack<> _elem;
stack<> _min;
};
void pop()
if
top
top
pop
pop
int top()
return
top
int getMin()
return
top
private
int
int
1.2.2 示例与输出
int main() {
MinStack minStack;
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
cout << "Current Min: " << minStack.getMin() << endl;
minStack.pop();
cout << "Top: " << minStack.top() << endl;
cout << "Current Min: " << minStack.getMin() << endl;
return 0;
}
Current Min: -3
Top: 0
Current Min: -2
在这个例子中,我们实现了一个最小栈,可以在常量时间内获取栈中的最小值。在压栈和弹栈操作时,我们同步更新 _min 栈以维护最小值。
1.3 栈的模拟实现
栈的标准实现使用 std::stack 容器,但在某些场景下,可以使用 std::vector 来模拟栈的功能。由于栈的所有操作都围绕末端进行,因此 vector 的尾插操作与 stack 类似。
#include <vector>
using namespace std;
template<typename T>
class SimulatedStack {
public:
void push(const T& x) {
_data.push_back(x);
}
void pop() {
_data.pop_back();
}
T& top() {
return _data.back();
}
bool empty() const {
return _data.empty();
}
size_t size() const {
return _data.size();
}
private:
vector<T> _data;
};
这种模拟实现使用了 vector 的尾插操作,可以高效地模拟栈的行为。
第二章:队列的介绍与使用
2.1 队列的介绍
队列 (Queue) 是一种 先进先出 (FIFO, First In First Out) 的数据结构。这意味着第一个进入队列的元素会第一个被移出。队列通常用于任务调度、广度优先搜索 (BFS) 等场景。
- push():将元素插入队尾。
- pop():移除队头元素。
- front():获取队头元素。
- back():获取队尾元素。
- empty():检查队列是否为空。
- size():获取队列中的元素数量。
2.2 队列的使用
#include <queue>
#include <iostream>
using namespace std;
int main() {
queue<int> q;
q.push(1);
q.push(2);
cout << "Front: " << q.front() << endl;
cout << "Back: " << q.back() << endl;
q.pop();
cout << "After pop, Front: " << q.front() << endl;
return 0;
}
2.2.1 示例与输出
Front: 1
Back: 2
After pop, Front: 2
在这个例子中,我们展示了队列的 push()、pop()、front() 和 back() 操作。
2.3 队列的模拟实现
在一些场景中,标准库中的 std::queue 可能无法满足特定需求,我们可以通过其他容器类来模拟实现队列。由于队列需要支持在头部删除和尾部插入的操作,使用 std::list 会比 std::vector 更为高效。list 允许在常数时间内进行头部和尾部的插入与删除操作,因此非常适合用于队列的实现。
#include <list>
using namespace std;
template<typename T>
class SimulatedQueue {
public:
void push(const T& x) {
_data.push_back(x);
}
void pop() {
_data.pop_front();
}
T& front() {
return _data.front();
}
T& back() {
return _data.back();
}
bool empty() const {
return _data.empty();
}
size_t size() const {
return _data.size();
}
private:
list<T> _data;
};
2.3.1 示例与输出
int main() {
SimulatedQueue<int> q;
q.push(10);
q.push(20);
cout << "队头: " << q.front() << endl;
cout << "队尾: " << q.back() << endl;
q.pop();
cout << "新的队头: " << q.front() << endl;
return 0;
}
在这个例子中,SimulatedQueue 模拟了队列的基本功能,包括在队尾插入元素和移除队头元素。
第三章:优先队列的介绍与使用
3.1 优先队列的介绍
优先队列 (Priority Queue) 是一种特殊类型的队列,其中每个元素都关联有一个优先级。优先队列的特点是元素的弹出顺序不再是按照先进先出,而是按照元素的优先级来决定。通常优先队列可以用来模拟堆结构。
在默认情况下,C++ 标准库中的 std::priority_queue 是一个大顶堆,即优先队列中的最大元素会优先弹出。我们也可以通过自定义比较函数来实现小顶堆,从而使得最小元素优先弹出。
- push():向优先队列中插入元素。
- pop():移除优先级最高的元素。
- top():获取优先级最高的元素。
- empty():检查优先队列是否为空。
- size():获取优先队列中的元素数量。
3.2 优先队列的使用
下面展示了如何使用 std::priority_queue 进行优先队列操作。
3.2.1 示例:默认大顶堆
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int main() {
priority_queue<int> pq;
pq.push(10);
pq.push(5);
pq.push(20);
cout << "优先级最高的元素:" << pq.top() << endl;
pq.pop();
cout << "新的优先级最高的元素:" << pq.top() << endl;
return 0;
}
3.2.2 示例与输出
优先级最高的元素:20
新的优先级最高的元素:10
在这个例子中,priority_queue<int> 实现了一个大顶堆,插入元素后,优先级最高的元素(值最大的元素)会优先弹出。
3.2.3 示例:小顶堆
我们也可以使用 std::greater<T> 来改变默认的比较方式,从而实现小顶堆。下面是一个小顶堆的例子:
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int main() {
priority_queue<int, vector<int>, greater<int>> pq;
pq.push(10);
pq.push(5);
pq.push(20);
cout << "优先级最高的元素(最小值):" << pq.top() << endl;
pq.pop();
cout << "新的优先级最高的元素:" << pq.top() << endl;
return 0;
}
优先级最高的元素(最小值):5
新的优先级最高的元素:10
在这个例子中,priority_queue<int, vector<int>, greater<int>> 实现了一个小顶堆,其中优先级最高的元素是值最小的元素。
3.3 优先队列的模拟实现
优先队列通常是基于堆实现的。在 C++ 中,标准库中的 priority_queue 使用 std::vector 作为底层存储,并通过堆算法管理优先级。在需要自定义优先队列行为时,可以自己实现一个简单的优先队列,利用堆的概念来操作。
下面是一个基于 std::vector 实现的简单优先队列:
#pragma once
#include <iostream>
#include <vector>
using namespace std;
namespace W {
template<class T>
struct less {
bool operator()(const T& left, const T& right) {
return left < right;
}
};
template<class T>
struct greater {
bool operator()(const T& left, const T& right) {
return left > right;
}
};
template<class T, class Container = std::vector<T>, class Compare = less<T>>
class priority_queue {
public:
priority_queue() : c() {}
template<class Iterator>
priority_queue(Iterator first, Iterator last) : c(first, last) {
int count = c.size();
int root = ((count - 2) >> 1);
for (; root >= 0; root--) AdjustDown(root);
}
void push(const T& data) {
c.push_back(data);
AdjustUP(c.size() - 1);
}
void pop() {
if (empty()) return;
swap(c.front(), c.back());
c.pop_back();
AdjustDown(0);
}
size_t size() const {
return c.size();
}
bool empty() const {
return c.empty();
}
const T& top() const {
return c.front();
}
private:
void AdjustUP(int child) {
int parent = ((child - 1) >> 1);
while (child) {
if (Compare()(c[parent], c[child])) {
swap(c[child], c[parent]);
child = parent;
parent = ((child - 1) >> 1);
} else {
return;
}
}
}
void AdjustDown(int parent) {
size_t child = parent * 2 + 1;
while (child < c.size()) {
if (child + 1 < c.size() && Compare()(c[child], c[child + 1])) {
child += 1;
}
if (Compare()(c[parent], c[child])) {
swap(c[child], c[parent]);
parent = child;
child = parent * 2 + 1;
} else {
return;
}
}
}
private:
Container c;
};
}
3.3.1 示例与输出
void TestQueuePriority() {
W::priority_queue<int> q1;
q1.push(5);
q1.push(1);
q1.push(4);
q1.push(2);
q1.push(3);
q1.push(6);
cout << "优先级最高的元素:" << q1.top() << endl;
q1.pop();
q1.pop();
cout << "新的优先级最高的元素:" << q1.top() << endl;
vector<int> v{5, 1, 4, 2, 3, 6};
W::priority_queue<int, vector<int>, W::greater<int>> q2(v.begin(), v.end());
cout << "优先级最高的元素 (最小值): " << q2.top() << endl;
q2.pop();
q2.pop();
cout << "新的优先级最高的元素 (最小值): " << q2.top() << endl;
}
优先级最高的元素:6
新的优先级最高的元素:4
优先级最高的元素 (最小值): 1
新的优先级最高的元素 (最小值): 3
在这个模拟实现中,我们使用 std::vector 作为底层容器,并且通过堆排序算法来维护优先队列的顺序。通过 less 和 greater 函数对象,我们可以分别实现大顶堆和小顶堆。
第四章:容器适配器
4.1 什么是容器适配器
容器适配器 (Container Adapter) 是一种设计模式,目的是将一种容器包装为另一种接口形式。容器适配器提供了一组特定的成员函数来访问底层的容器。C++ 标准库中,stack、queue 和 priority_queue 都是容器适配器,它们对容器的操作进行了限制,并定义了特定的访问规则。
容器适配器本质上是对基础容器的封装,它们可以使用 vector、deque、list 等作为底层实现,而具体使用哪种容器可以根据需求进行调整。容器适配器只暴露了某些特定的操作,而底层容器的更多操作则被隐藏。
虽然 stack 和 queue 中也可以存放元素,但在 STL 中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为 stack 和队列只是对其他容器的接口进行了包装,STL 中 stack 和 queue 默认使用 deque
- queue:实现先进先出(FIFO)原则。
- stack:实现后进先出(LIFO)原则。
- priority_queue:基于优先级进行元素的弹出,通常是大顶堆。
4.2 deque 的简单介绍
双端队列 (deque, Double-Ended Queue) 是一种可以在头尾两端进行高效插入和删除操作的序列式容器。与 vector 类似,deque 提供了动态数组的功能,但它比 vector 更加灵活,允许在头尾两端都进行元素的添加和删除。
4.2.1 deque 的原理介绍
- 双端操作:
deque 提供了双端操作,即允许在容器的两端进行插入和删除操作。其时间复杂度为常数时间 O(1),这使得它比 vector 更适合需要频繁在两端插入或删除元素的场景。
- 非连续存储:虽然
deque 表面上看起来像是一个连续的数组,但它的底层实现并非真正连续,而是由多块小的连续内存块组合而成,这就允许 deque 在进行元素插入或删除时,不需要像 vector 一样移动大量元素。
-------------------------------- | Block 1 | Block 2 | Block 3 | --------------------------------
每个块表示 deque 底层的一部分内存,插入或删除元素时,只需要操作相应块中的数据,而不需要重新分配整个数组的内存。
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其'整体连续'以及随机访问的假象,落在了 deque 的迭代器身上,因此 deque 的迭代器设计就比较复杂,如下图所示:
那 deque 是如何借助其迭代器维护其假想连续的结构呢?
4.2.2 deque 的缺陷
虽然 deque 在插入和删除操作上比 vector 更高效,但它也有一些缺点:
- 遍历性能较低:由于
deque 的存储结构不是一个连续的内存块,所以在遍历 deque 时,迭代器需要处理内存块之间的跳转,导致遍历性能不如 vector。
- 内存利用率稍低:因为
deque 是由多个小内存块组成的,所以它的内存利用率相比 vector 稍差。不过与 list 比较,deque 的内存效率还是更高。
4.2.3 deque 的常见操作
#include <deque>
#include <iostream>
using namespace std;
int main() {
deque<int> d;
d.push_back(10);
d.push_back(20);
d.push_front(5);
cout << "队头:" << d.front() << endl;
cout << "队尾:" << d.back() << endl;
d.pop_front();
d.pop_back();
cout << "新的队头:" << d.front() << endl;
return 0;
}
这个示例展示了 deque 如何进行两端的插入和删除操作。可以看到,deque 支持同时在队头和队尾进行高效的操作。
4.3 为什么选择 deque 作为 stack 和 queue 的底层默认容器
C++ 标准库中,stack 和 queue 的底层容器默认使用 deque,这是因为 deque 在元素插入和删除时的性能优势以及空间利用率较高的特点,正好符合 stack 和 queue 对容器的需求。
- 高效的尾部插入和删除:对于
stack,只需要支持在容器的尾部插入和删除元素,deque 的 push_back() 和 pop_back() 操作可以在常数时间内完成,比 vector 在扩容时效率更高。
- 高效的头部插入和删除:对于
queue,需要在尾部插入元素、在头部删除元素,deque 同时支持 push_back() 和 pop_front() 操作,效率比 list 高,且不需要存储额外的指针信息。
虽然 vector 也可以作为 stack 的底层容器,但它在尾部插入元素时需要在扩容时移动大量元素,因此效率不如 deque。而 list 虽然也支持两端插入删除操作,但由于需要存储额外的指针信息,空间利用率不如 deque 高。因此,deque 被认为是 stack 和 queue 的默认最佳选择。
4.4 STL 标准库中 stack 和 queue 的模拟实现
4.4.1 stack 的模拟实现
stack 的模拟实现只需要封装一个可以支持末端插入和删除操作的容器,默认情况下,使用 deque 作为底层容器。
#include <deque>
namespace W {
template<typename T, typename Container = deque<T>>
class stack {
public:
stack() {}
void push(const T& x) {
_c.push_back(x);
}
void pop() {
_c.pop_back();
}
T& top() {
return _c.back();
}
bool empty() const {
return _c.empty();
}
size_t size() const {
return _c.size();
}
private:
Container _c;
};
}
4.4.2 queue 的模拟实现
queue 的实现需要支持队尾插入元素、队头删除元素的操作。类似 stack,queue 默认使用 deque 作为底层容器。
#include <deque>
namespace W {
template<typename T, typename Container = deque<T>>
class queue {
public:
queue() {}
void push(const T& x) {
_c.push_back(x);
}
void pop() {
_c.pop_front();
}
T& front() {
return _c.front();
}
T& back() {
return _c.back();
}
bool empty() const {
return _c.empty();
}
size_t size() const {
return _c.size();
}
private:
Container _c;
};
}
第五章:总结
在本文中,我们详细探讨了 栈 (stack) 和 队列 (queue) 的概念、应用及其实现方式。通过对 deque 和 priority_queue 的深入理解,我们可以更高效地解决实际编程问题。
5.1 核心要点回顾
- 栈是一种后进先出的数据结构,常用于表达式求值、函数调用栈等。
- 队列是一种先进先出的数据结构,广泛应用于任务调度、广度优先搜索等场景。
- 优先队列根据元素的优先级进行排序,常用于调度和路径优化算法。
deque 作为双端队列,支持在头尾两端进行高效的插入和删除操作。
- 容器适配器
stack 和 queue 使用 deque 作为底层容器,利用其高效的插入删除操作实现栈和队列的功能。
通过对这些数据结构的深入理解,我们能够在编程中更加灵活、准确地选择合适的工具来解决实际问题。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online