1.1 stack的介绍
stack 是一种后进先出(LIFO, Last In First Out)的容器适配器,属于标准模板库(STL)中的一种。它仅允许在栈顶进行插入和删除操作,保证了数据访问的顺序性。stack 底层通常由 deque、list 或 vector 实现,具体取决于编译器实现,但用户无需关心底层细节。
其核心特性包括:
- 只能通过
top()访问栈顶元素。 - 元素只能从栈顶压入(
push)或弹出(pop)。 - 不支持随机访问或遍历。
- 适用于递归模拟、表达式求值、括号匹配、函数调用栈等场景。
1.2 stack的使用
| 函数说明 | 接口说明 |
|---|---|
| stack() | 构造一个空的栈。若未指定容器类型,默认使用 deque 作为底层容器。可显式指定如 stack<int, vector<int>> s; 来改变底层结构。 |
| empty() | 判断栈是否为空。返回 true 表示无元素,false 表示有至少一个元素。常用于防止对空栈调用 top() 引发未定义行为。 |
| size() | 返回当前栈中元素的数量。时间复杂度为 O(1),因内部维护了计数器。 |
| top() | 返回栈顶元素的引用。注意:不能对空栈调用此函数,否则会导致未定义行为。 使用前务必检查 empty()。 |
| push(const T& val) | 将元素 val 压入栈顶。该操作会复制 val 到栈中,若 val 是对象,则调用拷贝构造函数。 |
| pop() | 移除栈顶元素。仅移除,不返回值。 不能对空栈调用,否则程序行为异常。 |
重点强调:
top()和pop()必须在非空栈上调用,否则引发运行时错误。stack不提供迭代器,无法遍历所有元素。- 适合用于实现递归逻辑的非递归版本,如树的深度优先遍历、路径回溯等。
2.1 queue的介绍
queue 是一种先进先出(FIFO, First In First Out)的容器适配器,同样属于 STL。它只允许在队尾插入元素,在队头删除元素,保证了数据处理的顺序性。
典型应用场景包括:
- 消息队列
- 广度优先搜索(BFS)
- 缓冲区管理
- 任务调度系统
queue 的底层实现通常基于 deque,也可自定义为 list 等容器,但接口保持一致。
2.2 queue的使用
| 函数声明 | 接口说明 |
|---|---|
| queue() | 构造一个空队列。可指定底层容器类型,例如 queue<int, list<int>> q; 以使用 list 作为底层存储。 |
| empty() | 检测队列是否为空。返回 true 表示无元素,false 表示有元素。是安全使用 front() / back() 的前提。 |
| size() | 返回队列中有效元素的个数。时间复杂度为 O(1),内部维护计数。 |
| front() | 返回队头元素的引用。不可对空队列调用,否则导致未定义行为。 用于获取最早进入的元素。 |
| back() | 返回队尾元素的引用。同样需确保队列非空。 用于查看最新加入的元素。 |
| push(const T& val) | 在队尾插入新元素 val。执行复制构造,若为对象则触发拷贝语义。 |
| pop() | 移除队头元素。仅移除,不返回值。 调用前必须确认队列非空,否则程序崩溃。 |
重点强调:
front()与back()都要求队列非空,调用前应始终检查empty()。queue不支持随机访问或遍历,不能使用迭代器。- 在图的广度优先搜索(BFS)中,
queue是实现层级遍历的核心工具。- 若需频繁在头部删除元素且性能敏感,可考虑使用
std::deque直接实现,避免封装开销。
3. stack的模拟实现
栈(stack)是一种后进先出(LIFO, Last In First Out)的数据结构,其核心操作仅限于对尾部 (即栈顶)进行插入与删除。在C++中,std::stack 是一个适配器容器,它基于其他容器(如 deque、vector、list)实现,但默认使用 deque 作为底层容器。
3.1 为什么所有操作都在尾部?
栈的操作逻辑决定了只能在末尾 进行压入(push)和弹出(pop),并且访问的是最后一个元素 (top)。因此,无论底层使用哪种容器,都应将栈顶固定在容器的尾端 ,并统一使用 back()、push_back()、pop_back() 等接口。
cpp
// 模拟 stack 的基本结构(以 vector 为底层)
template <typename T>
class MyStack {
private:
std::vector<T> data;
public:
// 构造函数:创建空栈
MyStack() = default;
// 判空
bool empty() const {
return data.empty();
}
// 返回元素个数
size_t size() const {
return data.size();
}
// 访问栈顶元素(返回引用)
T& top() {
if (empty()) {
throw std::runtime_error("Stack is empty!");
}
return data.back(); // 直接访问尾部
}
const T& top() const {
if (empty()) {
throw std::runtime_error("Stack is empty!");
}
return data.back();
}
// 压入元素到栈顶(尾部)
void push(const T& val) {
data.push_back(val);
}
// 弹出栈顶元素(尾部移除)
void pop() {
if (empty()) {
throw std::runtime_error("Cannot pop from empty stack!");
}
data.pop_back();
}
};
✅ 重点说明:
- 所有操作均围绕
data.back()展开。- 使用
vector时,push_back与pop_back都是 O(1) 平摊时间复杂度。- 若用
list,虽然每次分配节点开销大,但操作仍是常数时间。deque在性能和内存布局上更优,是标准库默认选择。
3.2 底层容器的选择分析
| 容器类型 | 是否适合做 stack 底层 | 原因 |
|---|---|---|
vector |
✅ 推荐 | 内存连续,缓存友好;尾部操作 push_back / pop_back 为 O(1) 摊还 |
list |
✅ 可行 | 不需要搬移数据,但每次动态分配节点,性能略差 |
deque |
✅✅ 标准推荐 | 分段连续,两端操作均为 O(1),且支持高效扩容 |
📌 关键点 :
std::stack默认模板参数为std::deque<T>,即:
cppusing stack = std::stack<int>; // 等价于 std::stack<int, std::deque<int>>这意味着你只需写
stack<int>,无需显式指定容器。
3.3 为什么不使用 vector 的头部?
如果试图把栈顶放在 vector 的前端,那么:
push要调用insert(data.begin(), val)→ O(n)pop要调用erase(data.begin())→ O(n)
这会导致整个栈操作变为线性时间,严重违背 LIFO 的设计初衷。
❌ 错误做法示例:
cppdata.insert(data.begin(), val); // O(n),不可取
✅ 正确做法始终是尾部操作。
3.4 时间复杂度总结
| 操作 | 复杂度 | 说明 |
|---|---|---|
push() |
O(1) | 向尾部添加元素 |
pop() |
O(1) | 移除尾部元素 |
top() |
O(1) | 查看尾部元素 |
empty() |
O(1) | 检查是否为空 |
size() |
O(1) | vector.size() 本身为常数时间 |
⚠️ 注意:
vector在扩容时会触发整体复制,但这是摊还平均为 O(1),不影响整体复杂度。
4. queue的模拟实现
队列(queue)是一种先进先出(FIFO, First In First Out)的数据结构,特点是从一端入队,从另一端出队。
4.1 与 stack 的本质区别
| 特性 | Stack(栈) | Queue(队列) |
|---|---|---|
| 插入位置 | 尾部(back) | 尾部(back) |
| 删除位置 | 尾部(back) | 头部(front) |
| 操作方式 | LIFO | FIFO |
🔥 核心差异在于:
pop()对应的是pop_front()而非pop_back()。
cpp
// 模拟 queue(以 deque 为底层)
template <typename T>
class MyQueue {
private:
std::deque<T> data;
public:
MyQueue() = default;
bool empty() const {
return data.empty();
}
size_t size() const {
return data.size();
}
// 返回队头元素
T& front() {
if (empty()) {
throw std::runtime_error("Queue is empty!");
}
return data.front();
}
const T& front() const {
if (empty()) {
throw std::runtime_error("Queue is empty!");
}
return data.front();
}
// 返回队尾元素
T& back() {
if (empty()) {
throw std::runtime_error("Queue is empty!");
}
return data.back();
}
const T& back() const {
if (empty()) {
throw std::runtime_error("Queue is empty!");
}
return data.back();
}
// 入队:在尾部插入
void push(const T& val) {
data.push_back(val);
}
// 出队:从头部删除
void pop() {
if (empty()) {
throw std::runtime_error("Cannot pop from empty queue!");
}
data.pop_front();
}
};
✅ 该实现中,
push_back()与pop_front()都是常数时间操作。
4.2 为什么不能用 vector 做 queue 底层?
vector 的 pop_front() 是 O(n) 操作,因为要将所有元素向前移动一位。
cpp
// ❌ 错误示范:用 vector 做 queue
std::vector<int> q;
q.push_back(1); // OK: O(1)
q.pop_front(); // ❌ O(n)!效率极低
💡 结论:vector 不能用于实现队列,除非只做单向操作。
4.3 为什么选择 deque 作为默认底层?
deque(双端队列)内部结构如下:
- 由多个固定大小的缓冲区组成;
- 通过指针数组连接这些缓冲区;
- 支持在首尾两端高效插入/删除;
- 任意一端扩展时,只需新增一块缓冲区,无需搬移原有数据;
- 因此
push_front、pop_front、push_back、pop_back均为 O(1)。
✅
std::queue默认使用std::deque<T>,正是因为它能完美满足队列的两端操作需求。
4.4 环形缓冲区(数组队列)实现(面试高频考点)
这是一种不依赖动态容器的高性能队列实现方式,适用于固定容量场景。
cpp
template <typename T, size_t Capacity>
class CircularQueue {
private:
T buffer[Capacity];
size_t head = 0; // 指向队头
size_t tail = 0; // 指向下一个可写位置
size_t count = 0; // 当前元素数量
public:
bool empty() const { return count == 0; }
bool full() const { return count == Capacity; }
void push(const T& val) {
if (full()) {
throw std::runtime_error("Queue is full!");
}
buffer[tail] = val;
tail = (tail + 1) % Capacity;
++count;
}
void pop() {
if (empty()) {
throw std::runtime_error("Queue is empty!");
}
head = (head + 1) % Capacity;
--count;
}
T& front() {
if (empty()) {
throw std::runtime_error("Queue is empty!");
}
return buffer[head];
}
const T& front() const {
if (empty()) {
throw std::runtime_error("Queue is empty!");
}
return buffer[head];
}
T& back() {
if (empty()) {
throw std::runtime_error("Queue is empty!");
}
return buffer[(tail - 1) % Capacity];
}
size_t size() const { return count; }
};
✅ 优点:
内存固定,无动态分配;
所有操作均为常数时间;
适合嵌入式或实时系统。
⚠️ 注意事项:使用"牺牲一个位置"来区分空满状态(即
full()条件为count == Capacity);取模运算保证循环访问;
满时需扩容(重新分配数组并复制数据),代价较高。
4.5 时间复杂度总结
| 操作 | 复杂度 | 说明 |
|---|---|---|
push() |
O(1) | 尾部插入 |
pop() |
O(1) | 头部删除 |
front() |
O(1) | 获取队头 |
back() |
O(1) | 获取队尾 |
empty() |
O(1) | 判空 |
size() |
O(1) | 维护计数器 |
✅ 当底层为
deque或list时,全部操作为 O(1)。
5. stack 与 queue 的对比
| 对比维度 | Stack(栈) | Queue(队列) |
|---|---|---|
| 数据访问原则 | LIFO(后进先出) | FIFO(先进先出) |
| 插入位置 | 尾部(top) | 尾部(back) |
| 删除位置 | 尾部(top) | 头部(front) |
| 主要用途 | 表达式求值、递归回溯、括号匹配、函数调用栈 | 消息队列、广度优先搜索(BFS)、任务调度 |
| 典型应用场景 | 函数调用栈、撤销操作、表达式解析 | 缓冲处理、打印队列、网络请求排队 |
| 底层容器要求 | 支持尾部增删(vector/list/deque) | 必须支持头部删除(list/deque) |
| 能否用 vector? | ✅ 可以(尾部操作) | ❌ 不推荐(pop_front() 为 O(n)) |
| STL 默认容器 | deque |
deque |
| 操作复杂度 | 所有操作均为 O(1) | 所有操作均为 O(1)(底层为 deque/list) |
总结要点(重点提炼)
- stack 的核心是"尾部操作",所有接口都基于
back()系列; - queue 的核心是"两端操作",必须支持高效的
pop_front(); vector适合栈,不适合队列;deque是两者共同的理想底层容器;list虽然性能稍差,但也能胜任两种结构;- 环形队列是面试中考察数据结构设计的重要题目;
std::stack与std::queue都是适配器,可通过模板自定义底层容器;- 实际开发中,直接使用标准库即可,无需手动实现,但理解原理有助于调试与优化。
🧠 提醒:掌握这些底层机制,才能写出高效、安全的代码,尤其在算法竞赛、系统编程、嵌入式开发中至关重要。