C++ STL 中的 stack、queue 与 priority_queue:从使用到容器适配器原理

        在 C++ STL 中,stack(栈)、queue(队列)和 priority_queue(优先队列)并非传统意义上的 “容器”,而是容器适配器—— 它们通过封装底层容器(如 vector、list、deque)的接口,实现特定的数据访问规则(如 LIFO、FIFO、优先级排序)。本文将从 “使用→模拟实现→底层原理” 的路径,系统讲解这三种适配器的核心特性,并揭秘 STL 选择 deque 作为默认底层容器的深层原因。

一、stack:后进先出(LIFO)的栈适配器

1.1 认识 stack

stack 遵循 “后进先出”(Last In First Out)规则,仅允许在 “栈顶” 进行插入(push)和删除(pop)操作,无法直接访问栈中间的元素。其核心场景包括:表达式求值(如逆波兰表达式)、函数调用栈、括号匹配等。

STL 中 stack 的底层默认容器是deque,也可指定为 vector 或 list(只要底层容器支持push_back()pop_back()back()等接口)。

1.2 stack 核心接口实战

stack 的接口简洁,仅围绕 “栈顶操作” 和 “状态查询” 设计,具体如下:

函数声明接口说明代码示例
stack()无参构造,创建空栈stack<int> st;
empty()判断栈是否为空(空返回 true)if (st.empty()) { cout << "栈空"; }
size()返回栈中元素个数cout << "栈大小:" << st.size();
top()返回栈顶元素的引用(不删除)cout << "栈顶元素:" << st.top();
push(const T& val)栈顶插入元素 valst.push(10);(10 成为新栈顶)
pop()删除栈顶元素(无返回值)st.pop();(栈顶元素出栈)

注意:pop()仅删除栈顶元素,若需获取栈顶元素,需先调用top()

1.3 stack 经典 OJ 案例

案例 1:最小栈(LeetCode 155)

需求:设计一个栈,支持pushpoptop操作,并能在 O (1) 时间内检索到最小元素。思路:用两个栈实现 ——

  _elem栈:存储所有元素,实现基本栈操作;

  _min栈:存储当前栈中的最小值(仅当新元素≤当前最小值时入栈,出栈时若与_elem栈顶元素相等则同步出栈)。

代码实现:

class MinStack {
public:
    void push(int x) {
        _elem.push(x); // 所有元素入_elem栈
        // 新元素≤当前最小值时,入_min栈
        if (_min.empty() || x <= _min.top()) {
            _min.push(x);
        }
    }

    void pop() {
        // 若出栈元素是最小值,_min栈同步出栈
        if (_min.top() == _elem.top()) {
            _min.pop();
        }
        _elem.pop();
    }

    int top() {
        return _elem.top(); // 直接返回_elem栈顶
    }

    int getMin() {
        return _min.top(); // 返回_min栈顶(当前最小值)
    }

private:
    stack<int> _elem; // 存储所有元素
    stack<int> _min;  // 存储当前最小值
};
案例 2:逆波兰表达式求值(LeetCode 150)

需求:根据逆波兰表达式(后缀表达式)计算结果(如["2","1","+","3","*"]结果为(2+1)*3=9)。思路:遇到数字入栈,遇到运算符则弹出栈顶两个元素计算,结果重新入栈。

代码实现:

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        for (const string& token : tokens) {
            // 若为运算符,弹出两个元素计算
            if (token == "+" || token == "-" || token == "*" || token == "/") {
                int right = st.top(); st.pop(); // 右操作数(后入栈)
                int left = st.top(); st.pop();  // 左操作数(先入栈)
                switch (token[0]) {
                    case '+': st.push(left + right); break;
                    case '-': st.push(left - right); break;
                    case '*': st.push(left * right); break;
                    case '/': st.push(left / right); break; // 题目保证无除数为0
                }
            } else {
                // 若为数字,转换为int入栈
                st.push(atoi(token.c_str()));
            }
        }
        return st.top(); // 最终结果在栈顶
    }
};

1.4 stack 的模拟实现

stack 的本质是 “封装底层容器的尾操作”,因此只需选择一个支持push_back()pop_back()back()的容器(如 vector、deque),即可快速实现 stack。

以 vector 为底层容器的模拟实现:

#include <vector>
namespace bit {
template <class T, class Container = std::vector<T>> // 默认vector
class stack {
public:
    // 无参构造(底层容器默认构造)
    stack() {}

    // 栈顶插入
    void push(const T& x) { _container.push_back(x); }

    // 栈顶删除
    void pop() { _container.pop_back(); }

    // 获取栈顶元素
    T& top() { return _container.back(); }
    const T& top() const { return _container.back(); }

    // 栈状态查询
    bool empty() const { return _container.empty(); }
    size_t size() const { return _container.size(); }

private:
    Container _container; // 底层容器(被封装的核心)
};
}

二、queue:先进先出(FIFO)的队列适配器

2.1 认识 queue

queue 遵循 “先进先出”(First In First Out)规则,仅允许在 “队尾” 插入(push)、在 “队头” 删除(pop),无法访问队列中间的元素。其核心场景包括:任务调度、消息队列、广度优先搜索(BFS)等。

STL 中 queue 的底层默认容器是deque,也可指定为 list(需支持push_back()pop_front()front()back()接口)—— 但不能用 vector(vector 的pop_front()需搬移所有元素,效率极低)。

2.2 queue 核心接口实战

queue 的接口围绕 “队尾插入”“队头删除” 和 “状态查询” 设计,具体如下:

函数声明接口说明代码示例
queue()无参构造,创建空队列queue<int> q;
empty()判断队列是否为空(空返回 true)if (q.empty()) { cout << "队空"; }
size()返回队列中元素个数cout << "队列大小:" << q.size();
front()返回队头元素的引用(不删除)cout << "队头元素:" << q.front();
back()返回队尾元素的引用(不删除)cout << "队尾元素:" << q.back();
push(const T& val)队尾插入元素 valq.push(20);(20 成为新队尾)
pop()删除队头元素(无返回值)q.pop();(队头元素出队)

注意:pop()仅删除队头元素,若需获取队头元素,需先调用front()

2.3 queue 的模拟实现

queue 的核心是 “队尾插入 + 队头删除”,因此需选择支持push_back()pop_front()的底层容器(如 list、deque)。以 list 为底层容器的模拟实现如下:

#include <list>
namespace bit {
template <class T, class Container = std::list<T>> // 默认list
class queue {
public:
    // 无参构造
    queue() {}

    // 队尾插入
    void push(const T& x) { _container.push_back(x); }

    // 队头删除
    void pop() { _container.pop_front(); }

    // 获取队头/队尾元素
    T& front() { return _container.front(); }
    const T& front() const { return _container.front(); }
    T& back() { return _container.back(); }
    const T& back() const { return _container.back(); }

    // 队列状态查询
    bool empty() const { return _container.empty(); }
    size_t size() const { return _container.size(); }

private:
    Container _container; // 底层容器
};
}

三、priority_queue:优先级排序的队列适配器

3.1 认识 priority_queue

priority_queue(优先队列)是一种 “优先级驱动” 的队列,每次出队的不是队头元素,而是优先级最高的元素(默认是最大值,即 “大堆”)。其底层基于 “堆” 数据结构实现,默认使用 vector 作为底层容器(需支持随机访问迭代器以维护堆结构)。

核心场景包括:Top-K 问题(如数组中第 K 个最大元素)、任务调度(高优先级任务先执行)等。

3.2 priority_queue 核心接口实战

priority_queue 的接口与 queue 类似,但内部会自动维护堆结构,具体如下:

函数声明接口说明代码示例
priority_queue()无参构造,创建空优先队列(默认大堆)priority_queue<int> pq;
priority_queue(InputIterator first, InputIterator last)迭代器构造,用 [first, last) 区间元素初始化int arr[] = {3,1,2}; priority_queue<int> pq(arr, arr+3);
empty()判断优先队列是否为空if (pq.empty()) { cout << "队空"; }
size()返回元素个数cout << "队列大小:" << pq.size();
top()返回优先级最高的元素(堆顶,不删除)cout << "最高优先级元素:" << pq.top();
push(const T& val)插入元素,自动调整堆结构pq.push(4);(插入后仍为大堆)
pop()删除堆顶元素,自动调整堆结构pq.pop();(删除后仍为大堆)
关键配置:大堆与小堆

        默认大堆:底层用less<T>比较(需包含<functional>头文件),如priority_queue<int> pq;

        小堆:需显式指定三个模板参数:priority_queue<T, Container, greater<T>>,如priority_queue<int, vector<int>, greater<int>> pq;

自定义类型的优先级配置

若优先队列存储自定义类型(如Date),需重载operator<(大堆)或operator>(小堆),以定义优先级规则:

class Date {
public:
    Date(int year = 1900, int month = 1, int day = 1)
        : _year(year), _month(month), _day(day) {}

    // 重载<:用于大堆(按日期升序,旧日期优先级低)
    bool operator<(const Date& d) const {
        return (_year < d._year) ||
               (_year == d._year && _month < d._month) ||
               (_year == d._year && _month == d._month && _day < d._day);
    }

    // 重载>:用于小堆(按日期降序,新日期优先级低)
    bool operator>(const Date& d) const {
        return (_year > d._year) ||
               (_year == d._year && _month > d._month) ||
               (_year == d._year && _month == d._month && _day > d._day);
    }

    // 友元函数:支持cout输出
    friend ostream& operator<<(ostream& os, const Date& d) {
        os << d._year << "-" << d._month << "-" << d._day;
        return os;
    }

private:
    int _year, _month, _day;
};

// 测试:大堆与小堆
void TestPriorityQueue() {
    // 大堆(默认,按日期降序,最新日期优先级最高)
    priority_queue<Date> pq1;
    pq1.push(Date(2024, 5, 20));
    pq1.push(Date(2024, 5, 18));
    cout << "大堆顶(最新日期):" << pq1.top() << endl; // 2024-5-20

    // 小堆(按日期升序,最旧日期优先级最高)
    priority_queue<Date, vector<Date>, greater<Date>> pq2;
    pq2.push(Date(2024, 5, 20));
    pq2.push(Date(2024, 5, 18));
    cout << "小堆顶(最旧日期):" << pq2.top() << endl; // 2024-5-18
}

3.3 priority_queue 经典 OJ 案例:数组中第 K 个大的元素(LeetCode 215)

需求:找到数组中第 K 个最大的元素(如数组[3,2,1,5,6,4],K=2 时结果为 5)。思路:用大堆存储所有元素,弹出前 K-1 个最大值,剩余堆顶即为第 K 个最大值。

代码实现:

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        // 用数组初始化大堆
        priority_queue<int> pq(nums.begin(), nums.end());
        // 弹出前K-1个最大值
        for (int i = 0; i < k - 1; ++i) {
            pq.pop();
        }
        return pq.top(); // 剩余堆顶为第K个最大值
    }
};

四、容器适配器的底层原理:为什么默认选 deque?

4.1 什么是容器适配器?

适配器(Adapter)是一种设计模式,核心是 “接口转换”—— 将一个类的接口转换成客户期望的另一个接口。STL 中的 stack、queue、priority_queue 并非直接管理内存,而是通过封装底层容器的接口,实现特定的数据访问规则(如 LIFO、FIFO),因此被称为 “容器适配器”。

4.2 deque 的特性:stack 与 queue 的理想底层容器

deque(双端队列)是一种 “双开口” 的分段连续容器,支持:

        头尾两端 O (1) 时间插入 / 删除;

        随机访问(效率低于 vector,但高于 list);

        扩容时无需搬移大量元素(仅需拼接新的分段空间)。

其底层结构类似 “动态二维数组”:

        中控器(map):存储各分段空间(缓冲区)的指针;

        缓冲区:存储实际元素的连续小空间(默认大小为 512 字节)。

deque 的缺陷是遍历效率低(迭代器需频繁检测是否跨越缓冲区边界),但 stack 和 queue 恰好不需要遍历 —— 这使其成为两者的理想底层容器。

4.3 STL 选择 deque 作为默认容器的原因

  1. stack 的需求匹配:stack 仅需尾插(push_back())和尾删(pop_back()),deque 的尾操作效率与 vector 相当,且扩容时无需搬移大量元素(vector 扩容需拷贝旧元素),效率更高。

  2. queue 的需求匹配:queue 需尾插(push_back())和头删(pop_front()):

    • list 的头删效率虽高,但节点分散存储,空间利用率低;
    • deque 的头删无需搬移元素(直接操作首缓冲区),且空间利用率高于 list。
  3. 避开 deque 的缺陷:deque 的遍历效率低,但 stack 和 queue 均不支持迭代器(无需遍历),完美避开了这一缺陷。

五、总结

stack、queue、priority_queue 作为 STL 的容器适配器,其核心价值在于 “封装底层容器,实现特定访问规则”:

        stack:LIFO 规则,适合栈顶操作场景,默认底层容器为 deque;

        queue:FIFO 规则,适合头尾操作场景,默认底层容器为 deque;

        priority_queue:优先级规则,基于堆实现,默认底层容器为 vector(需随机访问维护堆结构)。

学习这三种适配器的关键在于:

  1. 能用:掌握核心接口,结合场景选择合适的适配器(如 Top-K 问题用 priority_queue);
  2. 明理:理解容器适配器的设计思想,以及底层容器的选择逻辑(如 deque 为何成为 stack/queue 的默认容器);
  3. 会扩展:能根据需求指定自定义底层容器(如 stack 用 vector、queue 用 list),甚至模拟实现适配器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值