stack和queue(类函数学习和模拟实现)

1.1 stack的介绍

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的文档介绍

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>,即:

cpp 复制代码
using 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 的设计初衷。

❌ 错误做法示例:

cpp 复制代码
data.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 都是适配器,可通过模板自定义底层容器;
  • 实际开发中,直接使用标准库即可,无需手动实现,但理解原理有助于调试与优化。

🧠 提醒:掌握这些底层机制,才能写出高效、安全的代码,尤其在算法竞赛、系统编程、嵌入式开发中至关重要。

相关推荐
.道阻且长.1 小时前
C++11 :右值引用的作用,引用折叠和完美转发
java·开发语言·c++
Vcaker1 小时前
Linux学习37-rook-ceph部署
linux·运维·学习
LHR_friendship2 小时前
HCIA的相关笔记
笔记·hcia
阳光九叶草LXGZXJ2 小时前
Linux-学习-12-JDK安装
java·linux·运维·学习
tiankongdeyige10 小时前
游戏战斗系统学习第2篇:有限状态机 (FSM)
学习·游戏·typescript
朔北之忘 Clancy11 小时前
2025 CSP-J 第二轮真题解析
c++·青少年编程·题解·noip·csp·信奥赛·noi
longlongzihan11 小时前
LeetCode 17电话号码的字母组合:回溯算法(DFS)详解
c++·算法·leetcode·深度优先
yolo_guo11 小时前
调试mysql延迟与libevent回调实际发送回复时机问题
c++·mysql·libevent
今晚打老虎12 小时前
c++之提高A(前缀和)(第三课)
数据结构·c++·算法