C++ STL stack、queue 与容器适配器详解

栈(stack)、队列(queue)和优先级队列(priority_queue)是C++ STL中非常经典的容器适配器,也是数据结构与算法面试、笔试中的高频考点。它们底层依赖其他容器实现,对外提供统一、简洁的接口,分别满足后进先出、先进先出、优先级优先三种不同的数据访问需求。

本文将从原理介绍、常用接口、模拟实现、经典面试题四个维度,详细梳理 stack、queue、priority_queue 的核心知识点,并附上可直接运行的代码实现,帮助大家彻底掌握这三种容器适配器。


1. stack(栈)的介绍与使用

1.1 stack 的本质

• 后进先出(LIFO):只能在栈顶(top)进行插入(push)和删除(pop)操作。

• 是一种容器适配器,而非独立容器,STL 默认使用 deque 作为底层容器。

1.2 stack 核心接口

|-----------|--------------|
| 函数声明 | 接口说明 |
| stack() | 构造空栈 |
| empty() | 检测栈是否为空 |
| size() | 返回栈中元素个数 |
| top() | 返回栈顶元素的引用 |
| push(val) | 将元素 val 压入栈顶 |
| pop() | 将栈顶元素弹出 |

代码示例:

cpp 复制代码
#include <iostream>
#include <stack>  // 必须包含此头文件

using namespace std;

int main() {
    // 1. stack() - 构造空栈
    stack<int> st;

    // 2. empty() - 检测栈是否为空
    if (st.empty()) {
        cout << "初始状态:栈为空" << endl;
    }

    // 3. push(val) - 将元素压入栈顶
    cout << "依次压入 10, 20, 30..." << endl;
    st.push(10);
    st.push(20);
    st.push(30);

    // 4. size() - 返回栈中元素个数
    cout << "当前栈的大小:" << st.size() << endl;

    // 5. top() - 返回栈顶元素的引用
    // 注意:top() 只获取不删除,栈不为空时才能调用
    cout << "栈顶元素是:" << st.top() << endl;

    // 修改变量(演示引用特性)
    st.top() = 999;
    cout << "修改栈顶元素后,新的栈顶是:" << st.top() << endl;

    // 6. pop() - 将栈顶元素弹出
    // 注意:pop() 只删除不返回,栈不为空时才能调用
    cout << "执行弹出操作..." << endl;
    st.pop();
    cout << "弹出后,新的栈顶是:" << st.top() << endl;
    cout << "弹出后,栈的大小:" << st.size() << endl;

    return 0;
}

运行结果:

cpp 复制代码
初始状态:栈为空
依次压入 10, 20, 30...
当前栈的大小:3
栈顶元素是:30
修改栈顶元素后,新的栈顶是:999
执行弹出操作...
弹出后,新的栈顶是:20
弹出后,栈的大小:2

⚠️注意:

  1. top() 与 pop() 的分工:STL 的设计理念是"职责单一"。top() 只负责读取栈顶,pop() 只负责删除栈顶。如果想"取出并删除",需要先 top() 再 pop()。

  2. 空栈调用风险:在调用 top() 和 pop() 之前,必须先用 empty() 判断栈是否为空,否则会导致程序崩溃(未定义行为)。

  3. 引用特性:top() 返回的是引用,因此可以直接修改栈顶元素的值(如示例中将 30 改为 999)。

1.3 经典例题:最小栈(MinStack)

设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。

思路:

最小栈的要求是:在 O(1) 时间内获取栈中最小值,同时支持正常的 push、pop、top 操作。
普通栈只能在栈顶操作,无法直接拿到最小值,因此需要用两个栈配合来实现。

  1. 整体设计:主栈 _st:存所有真实数据,保证正常的栈功能。辅助栈 _min:专门存当前状态下的最小值,栈顶永远是整个栈的最小值。
  2. push 思路:新元素先正常压入主栈。如果辅助栈为空,直接压入。如果新元素 ≤ 辅助栈栈顶,也压入辅助栈。用 ≤ 而不是 <,是为了处理重复最小值的情况,保证 pop 时不会出错。
  3. pop 思路:先看主栈栈顶是否等于辅助栈栈顶。如果相等,说明要弹出的是当前最小值,辅助栈也要一起弹出。最后主栈正常弹出。
  4. top / getMin 思路:top():直接返回主栈栈顶。getMin():直接返回辅助栈栈顶。两个操作都是 O(1)。
  5. 核心思想总结:用空间换时间:用一个额外的栈,专门维护每一步的最小值,让获取最小元素从遍历 O(n) 变成直接取栈顶 O(1)。
cpp 复制代码
#include <stack>
using namespace std;

/**
 * @brief 最小栈:支持 push、pop、top 操作,并能在常数时间 O(1) 内检索到最小元素。
 * @note 核心思路:使用两个栈,一个存储数据,一个存储当前的最小值序列。
 */
class MinStack {
public:
    /**
     * @brief 构造函数:默认构造即可,栈的初始化由其自身的构造函数完成。
     */
    MinStack() {}

    /**
     * @brief 入栈操作
     * @param val 要压入栈的数值
     */
    void push(int val) {
        // 1. 无论如何,先将数据压入主数据栈
        _st.push(val);

        // 2. 关键逻辑:维护最小值栈
        // 条件1:_min为空(首次入栈),必须压入
        // 条件2:新值小于等于当前最小值,也需要压入(小于等于处理重复最小值的情况)
        if (_min.empty() || val <= _min.top()) {
            _min.push(val);
        }
    }

    /**
     * @brief 出栈操作
     * @note 必须同步维护最小值栈,防止最小值被弹出后,_min栈顶失效。
     */
    void pop() {
        // 1. 关键判断:如果要弹出的元素是当前的最小值
        if (_st.top() == _min.top()) {
            // 则最小值栈也需要同步弹出栈顶
            _min.pop();
        }

        // 2. 主数据栈执行出栈
        _st.pop();
    }

    /**
     * @brief 获取栈顶元素
     * @return 栈顶元素的值
     * @note 直接返回主数据栈的栈顶
     */
    int top() {
        return _st.top();
    }

    /**
     * @brief 检索栈中的最小元素
     * @return 栈中的最小值
     * @note 时间复杂度 O(1),直接返回最小值栈的栈顶
     */
    int getMin() {
        return _min.top();
    }

private:
    stack<int> _st;   // 主栈:存储所有入栈的元素
    stack<int> _min;  // 辅助栈:存储对应主栈状态下的最小值,栈顶始终是当前的最小值
};

1.4 经典例题:栈的压入弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。

  1. 0<=pushV.length == popV.length <=1000

  2. -1000<=pushV[i]<=1000

  3. pushV 的所有数字均不相同

思路:

• 用一个辅助栈 st 模拟真实栈的操作。

• 遍历入栈序列 pushV,逐个将元素压入栈。

• 每次压入后,检查栈顶是否与当前需要弹出的元素(popV[popi])一致:

◦ 如果一致,就弹出栈顶,并移动弹出序列的指针 popi。

◦ 重复这个过程,直到栈顶不匹配或栈为空。

• 最后,如果辅助栈 st 为空,说明所有元素都按顺序弹出了,返回 true;否则返回 false。

cpp 复制代码
class Solution {
  public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param pushV int整型vector
     * @param popV int整型vector
     * @return bool布尔型
     */
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
        size_t popi = 0;          // 指向弹出序列 popV 的当前位置
        stack<int> st;            // 辅助栈,模拟真实栈的操作
        for (auto& e : pushV) {
            // 入栈
            st.push(e);
            // 跟出栈序列匹配
            while (!st.empty() && st.top() == popV[popi]) {
                st.pop();
                popi++;
            }
        }
        return st.empty();
        // 如果 st.empty() 为 true,说明所有入栈元素都被成功弹出,弹出序列合法。
        // 如果 st 不为空,说明还有元素无法按弹出序列顺序弹出,序列不合法。
    }
};

• 模拟法:严格按照栈的"后进先出"规则,模拟每一步操作,用结果验证序列的合法性。

• 时间复杂度:每个元素最多入栈和出栈一次,因此时间复杂度为 O(n)。

• 空间复杂度:最坏情况下(如入栈序列和弹出序列完全相反),需要 O(n) 的辅助栈空间。

1.5 经典例题:逆波兰表达式求值

利用栈计算后缀表达式(逆波兰式)的值。

cpp 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> s;
        for (size_t i = 0; i < tokens.size(); ++i) {
            string& str = tokens[i];
            // 如果是数字,入栈
            if (str != "+" && str != "-" && str != "*" && str != "/") {
                s.push(atoi(str.c_str()));
            } else {
                // 如果是运算符,弹出两个数计算
                int right = s.top(); s.pop();
                int left = s.top(); s.pop();

                switch (str[0]) {
                    case '+': s.push(left + right); break;
                    case '-': s.push(left - right); break;
                    case '*': s.push(left * right); break;
                    case '/': s.push(left / right); break; // 题目保证除数不为0
                }
            }
        }
        return s.top();
    }
};

1.6 二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

思路:用队列 + 记录每一层节点个数。

  1. 队列:用来按顺序存下一层要遍历的节点。

  2. levelSize:记录当前这一层一共有多少个节点,保证一层遍历完再遍历下一层。

  3. 每处理完一层,就把这一层的结果放进二维数组,最终返回。

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x, left(left), right(right) {}
 * };
 */

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        // 存放最终结果:每一层对应一个vector<int>
        vector<vector<int>> vv;

        // 队列:用于层序遍历,存储节点指针
        queue<TreeNode*> q;

        // 记录当前层有多少个节点
        int levelSize = 0;

        // 如果根节点不为空,先把根入队,第一层节点数为1
        if(root)
        {
            q.push(root);
            levelSize = 1;
        }

        // 队列不为空就继续遍历
        while(!q.empty())
        {
            // 用来存当前这一层的节点值
            vector<int> v;

            // 遍历当前层的所有节点(levelSize个)
            while(levelSize--)
            {
                // 取队头节点
                TreeNode* front = q.front();
                q.pop();

                // 把当前节点的值放入当前层数组
                v.push_back(front->val);

                // 左孩子不为空,入队(下一层)
                if(front->left)
                    q.push(front->left);
                
                // 右孩子不为空,入队(下一层)
                if(front->right)
                    q.push(front->right);
            }

            // 当前层遍历完,把这一层结果放进最终二维数组
            vv.push_back(v);

            // 更新下一层的节点个数 = 现在队列里的元素个数
            levelSize = q.size();
        }

        return vv;
    }
};

举例:

cpp 复制代码
        3
       / \
      9  20
        /  \
       15   7

我们要得到的层序遍历结果是:

[
  [3],
  [9,20],
  [15,7]
]

对着例子走一遍流程

1)第 0 层:[3] 队列:[3]; levelSize = 1

处理 3:存入 v → [3];左 9、右 20 入队; 队列变成:[9,20]; levelSize 更新为 2;vv 变成:[[3]]

2)第 1 层:[9,20] levelSize = 2

处理 9: 存入 v → [9];无孩子

处理 20:存入 v → [9,20];左 15、右 7 入队;队列变成:[15,7];levelSize 更新为 2;vv 变成:[[3], [9,20]]

3)第 2 层:[15,7] levelSize = 2

处理 15 → 存入 处理 7 → 存入 ;都没孩子,队列空; vv 变成:

cpp 复制代码
[
  [3],
  [9,20],
  [15,7]
]

• 数据结构:队列 + 二维数组

• 关键点:用 levelSize 控制每一层的遍历范围

• 时间复杂度:O(n),每个节点只访问一次

• 空间复杂度:O(n),队列最多存一层节点

1.7 stack的模拟实现

用 vector 模拟栈(仅尾插尾删,符合栈的特性):

cpp 复制代码
#include <vector>
namespace gxy {
    template<class T>
    class stack {
    public:
        stack() {}
        void push(const T& x) { _c.push_back(x); }
        void pop() { _c.pop_back(); }
        T& top() { return _c.back(); }
        const T& top() const { return _c.back(); }
        size_t size() const { return _c.size(); }
        bool empty() const { return _c.empty(); }
    private:
        std::vector<T> _c;
    };
}

stack(容器适配器版)

cpp 复制代码
#pragma once    // 防止头文件被重复包含
#include <deque> // 默认底层容器使用 deque

namespace gxy
{
    // 栈的模拟实现 ------ 容器适配器
    // 第二个模板参数 Container:底层存储的容器
    // 默认使用 deque<T>,也可以传 vector、list
    template<class T, class Container = deque<T>>
    class stack
    {
    public:
        // 入栈:尾插
        void push(const T& x)
        {
            // 底层容器调用尾插,适配成栈的push
            _con.push_back(x);
        }

        // 出栈:尾删
        void pop()
        {
            // 底层容器调用尾删,适配成栈的pop
            _con.pop_back();
        }

        // 获取栈顶元素(只读)
        const T& top() const
        {
            // 栈顶 = 底层容器的最后一个元素
            return _con.back();
        }

        // 获取栈中元素个数
        size_t size() const
        {
            return _con.size();
        }

        // 判断栈是否为空
        bool empty() const
        {
            return _con.empty();
        }

    private:
        // 底层存储的容器(适配器的核心:不自己实现结构,复用容器)
        Container _con;
    };
}

核心知识点整理

(1)这是「容器适配器」,不是自己实现数据结构

• stack 不直接实现内存管理; 复用已有的容器(deque / vector / list); 只开放栈需要的接口:push / pop / top 等; 封闭其他接口,实现后进先出 LIFO

(2)模板参数意义 template<class T, class Container = deque<T>>

T:栈里存储的数据类型 Container:底层存储的容器,默认是 deque

可以手动改成别的容器:stack<int, vector<int>> st1; stack<int, list<int>> st2;

(3)接口与底层容器对应关系

|---------|-------------------|------|
| stack接口 | 底层容器调用 | 功能 |
| push(x) | _con.push_back(x) | 入栈 |
| pop() | _con.pop_back() | 出栈 |
| top() | _con.back() | 获取栈顶 |
| size() | _con.size() | 元素个数 |
| empty() | _con.empty() | 是否为空 |

stack 是容器适配器,它不自己实现数据结构,只要求底层容器提供 push_back、pop_back、back,然后把这些接口封装成栈的 push、pop、top,从而实现后进先出。

(4)为什么默认用 deque?

头尾插入删除都是 O(1);扩容代价小,不需要拷贝大量数据;比 vector 更适合做栈、队列的底层容器

• vector:扩容要拷贝,效率一般;list:空间碎片多,缓存不友好;deque:头尾都快,扩容不拷贝,综合最好

2. queue(队列)的介绍与使用

2.1 queue 的本质

• 先进先出(FIFO):队尾(back)入队,队头(front)出队。

• 也是容器适配器,STL 默认使用 deque 作为底层容器。

2.2 queue 核心接口

|-----------|---------------|
| 函数声明 | 接口说明 |
| queue() | 构造空队列 |
| empty() | 检测队列是否为空 |
| size() | 返回队列中有效元素个数 |
| front() | 返回队头元素的引用 |
| back() | 返回队尾元素的引用 |
| push(val) | 在队尾将元素 val 入队 |
| pop() | 将队头元素出队 |

代码示例:

cpp 复制代码
#include <iostream>
#include <queue>
using namespace std;

int main() {
    // 1. queue() ------ 构造空队列
    queue<int> q;

    // 2. empty() ------ 检测队列是否为空
    if (q.empty()) {
        cout << "队列初始为空" << endl;
    }

    // 3. push(val) ------ 在队尾将元素 val 入队
    q.push(10);
    q.push(20);
    q.push(30);
    cout << "入队 10、20、30" << endl;

    // 4. size() ------ 返回队列中有效元素个数
    cout << "队列大小:" << q.size() << endl;

    // 5. front() ------ 返回队头元素的引用
    cout << "队头元素:" << q.front() << endl;

    // 6. back() ------ 返回队尾元素的引用
    cout << "队尾元素:" << q.back() << endl;

    // 修改队头、队尾(因为返回的是引用)
    q.front() = 1;
    q.back() = 3;
    cout << "修改后队头:" << q.front() << endl;
    cout << "修改后队尾:" << q.back() << endl;

    // 7. pop() ------ 将队头元素出队
    q.pop();
    cout << "执行一次出队后,新队头:" << q.front() << endl;
    cout << "队列大小:" << q.size() << endl;

    return 0;
}

运行结果:

cpp 复制代码
队列初始为空
入队 10、20、30
队列大小:3
队头元素:10
队尾元素:30
修改后队头:1
修改后队尾:3
执行一次出队后,新队头:20
队列大小:2

2.3 queue 的模拟实现

用 list 模拟队列(支持头删尾插,符合队列特性):

cpp 复制代码
#include <list>
namespace gxy {
    template<class T>
    class queue {
    public:
        queue() {}
        void push(const T& x) { _c.push_back(x); }
        void pop() { _c.pop_front(); }
        T& back() { return _c.back(); }
        const T& back() const { return _c.back(); }
        T& front() { return _c.front(); }
        const T& front() const { return _c.front(); }
        size_t size() const { return _c.size(); }
        bool empty() const { return _c.empty(); }
    private:
        std::list<T> _c;
    };
}

queue 容器适配器模拟实现

cpp 复制代码
#pragma once        // 防止头文件重复包含
#include <deque>    // 默认底层容器使用 deque

namespace gxy
{
    // 队列的模拟实现 ------ 容器适配器
    // 底层容器默认使用 deque<T>
    template<class T, class Container = deque<T>>
    class queue
    {
    public:
        // 入队:尾插
        void push(const T& x)
        {
            _con.push_back(x);
        }

        // 出队:头删
        void pop()
        {
            _con.pop_front();
        }

        // 获取队头元素(只读)
        const T& front() const
        {
            return _con.front();
        }

        // 获取队尾元素(只读)
        const T& back() const
        {
            return _con.back();
        }

        // 获取元素个数
        size_t size() const
        {
            return _con.size();
        }

        // 判断队列是否为空
        bool empty() const
        {
            return _con.empty();
        }

    private:
        Container _con;  // 底层存储容器
    };
}

核心说明

  1. queue 为什么是容器适配器?

自己不实现底层结构;复用已有容器的接口,封装成先进先出 FIFO

  1. 底层容器必须支持的接口

push_back 尾插; pop_front 头删; front 取队头; back 取队尾; size / empty

  1. 为什么不能用 vector 做 queue 底层?

因为 vector 不支持 pop_front()(或效率极低 O(N))。所以 queue 底层只能用:deque / list。

  1. 接口对应关系

|---------|-----------|------|
| queue接口 | 底层容器调用 | 功能 |
| push(x) | push_back | 入队 |
| pop() | pop_front | 出队 |
| front() | front() | 取队头 |
| back() | back() | 取队尾 |
| size() | size() | 元素个数 |
| empty() | empty() | 是否为空 |

3. priority_queue(优先级队列)的介绍与使用

3.1 priority_queue 的本质

• 是一个堆,默认是大顶堆(堆顶元素最大)。

• 也是容器适配器,默认使用 vector 作为底层容器,并在其上维护堆结构。

3.2 priority_queue 核心接口

|------------------|---------------|
| 函数声明 | 接口说明 |
| priority_queue() | 构造空优先级队列 |
| empty() | 检测是否为空 |
| size() | 返回元素个数 |
| top() | 返回堆顶元素(最大/最小) |
| push(x) | 插入元素并调整堆 |
| pop() | 删除堆顶元素并调整堆 |

代码示例:

cpp 复制代码
#include <iostream>
#include <queue>
using namespace std;

int main() {
    // 1. priority_queue() ------ 构造空优先级队列(默认:大顶堆)
    priority_queue<int> pq;

    // 2. empty() ------ 检测是否为空
    if (pq.empty()) {
        cout << "优先级队列初始为空" << endl;
    }

    // 3. push(x) ------ 插入元素并自动调整堆结构
    pq.push(5);
    pq.push(2);
    pq.push(8);
    pq.push(1);
    cout << "插入 5、2、8、1 完成" << endl;

    // 4. size() ------ 返回元素个数
    cout << "元素个数:" << pq.size() << endl;

    // 5. top() ------ 返回堆顶元素(大顶堆:最大值)
    cout << "堆顶元素:" << pq.top() << endl;

    // 6. pop() ------ 删除堆顶元素,并重新调整堆
    pq.pop();
    cout << "删除堆顶后,新堆顶:" << pq.top() << endl;

    // 再删一次看变化
    pq.pop();
    cout << "再删堆顶后,新堆顶:" << pq.top() << endl;

    return 0;
}

运行结果:

cpp 复制代码
优先级队列初始为空
插入 5、2、8、1 完成
元素个数:4
堆顶元素:8
删除堆顶后,新堆顶:5
再删堆顶后,新堆顶:2

小顶堆写法:

cpp 复制代码
// 小顶堆(堆顶是最小值)
priority_queue<int, vector<int>, greater<int>> pq;

3.3 大顶堆与小顶堆

• 大顶堆(默认):priority_queue<int> q;

• 小顶堆:需要指定比较方式 greater<T>:priority_queue<int, vector<int>, greater<int>> q;

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

// 仿函数:本质是一个类,重载 operator(),可以像函数一样使用
template<class T>
class Less
{
public:
	bool operator()(const T& x, const T& y)
	{
		return x < y;
	}
};

template<class T>
class Greater
{
public:
	bool operator()(const T& x, const T& y)
	{
		return x > y;
	}
};

// 用仿函数控制升序降序
template<class Compare>
void BubbleSort(int* a, int n, Compare com)
{
	for (int j = 0; j < n; j++)
	{
		int flag = 0;
		for (int i = 1; i < n - j; i++)
		{
			if (com(a[i], a[i - 1]))
			{
				swap(a[i - 1], a[i]);
				flag = 1;
			}
		}

		if (flag == 0)
			break;
	}
}

int main()
{
	Less<int> LessFunc;
	Greater<int> GreaterFunc;

	// 像函数一样使用
	cout << LessFunc(1, 2) << endl;
	cout << LessFunc.operator()(1, 2) << endl;

	int a[] = { 9,1,2,5,7,4,6,3 };

	// 升序
	BubbleSort(a, 8, LessFunc);
	// 降序
	BubbleSort(a, 8, GreaterFunc);

	// 匿名对象调用
	BubbleSort(a, 8, Less<int>());
	BubbleSort(a, 8, Greater<int>());

	return 0;
}

代码解释:

一、代码整体是干嘛的?为了讲清楚:什么是仿函数(函数对象)?为什么要有仿函数?怎么用?

并用冒泡排序演示:一套排序代码,通过传入不同仿函数,就能控制升序 / 降序。

二、逐段超详细解释

  1. 仿函数的定义
cpp 复制代码
// 仿函数:本质是一个类,重载 operator(),可以像函数一样使用
template<class T>
class Less
{
public:
    // 重载括号运算符
    bool operator()(const T& x, const T& y)
    {
        return x < y; // 小于
    }
};
template<class T>
class Greater
{
public:
    bool operator()(const T& x, const T& y)
    {
        return x > y; // 大于
    }
};

• 仿函数 = 重载了 operator() 的类; 它的对象可以像函数一样调用:LessFunc(1, 2);

• 作用:把"比较规则"封装成一个对象,方便传给算法。

  1. 模板冒泡排序(接收仿函数)
cpp 复制代码
template<class Compare>
void BubbleSort(int* a, int n, Compare com)
{
    for (int j = 0; j < n; j++)
    {
        int flag = 0;
        for (int i = 1; i < n - j; i++)
        {
            // 不再写死 a[i] < a[i-1] 或 >
            // 而是调用仿函数来判断
            if (com(a[i], a[i - 1]))
            {
                swap(a[i - 1], a[i]);
                flag = 1;
            }
        }
        if (flag == 0)
            break;
    }
}

• 以前排序:if (a[i] < a[i-1]) swap(); // 只能升序

• 现在:if (com(a[i], a[i-1])) 比较规则由外面传进来,排序逻辑不用改!

  1. main 函数使用
cpp 复制代码
int main()
{
    Less<int> LessFunc;       // 创建仿函数对象
    Greater<int> GreaterFunc;
    // 像函数一样调用,本质是:LessFunc.operator()(1,2)
    cout << LessFunc(1, 2) << endl;
    cout << LessFunc.operator()(1, 2) << endl;
    int a[] = { 9,1,2,5,7,4,6,3 };
    // 传 Less → 升序
    BubbleSort(a, 8, LessFunc);
    // 传 Greater → 降序
    BubbleSort(a, 8, GreaterFunc);
    // 直接用匿名对象(最常用)
    BubbleSort(a, 8, Less<int>());
    BubbleSort(a, 8, Greater<int>());
    return 0;
}
  1. 仿函数对象可以像函数一样调用 LessFunc(1,2)

  2. 一套排序,两种规则: 传 Less → 升序; 传 Greater → 降序

  3. 匿名对象写法(STL 标准风格)

cpp 复制代码
Less<int>()
Greater<int>()

三、总结

  1. 仿函数:一个类,重载了 operator(),对象可以像函数一样使用。

  2. 作用:把比较逻辑/策略封装成对象,传给算法。

  3. 好处:一套算法代码,支持多种比较规则,不用改源码。

  4. STL 到处都用:sort、priority_queue、map......全都靠仿函数控制规则。

3.4 priority_queue 模拟实现(大堆/小堆 + 仿函数 + 向上/向下调整)

priority_queue.h

cpp 复制代码
#pragma once

#include <vector>
#include <algorithm>  // swap

// 小于比较仿函数:控制建立 大堆
template<class T>
class Less
{
public:
    bool operator()(const T& x, const T& y)
    {
        return x < y;
    }
};

// 大于比较仿函数:控制建立 小堆
template<class T>
class Greater
{
public:
    bool operator()(const T& x, const T& y)
    {
        return x > y;
    }
};

namespace gxy
{
    // 优先级队列模拟实现(底层是堆)
    // 默认容器:vector
    // 默认比较方法:Less<T> → 建立 大顶堆
    template<class T, class Container = vector<T>, class Compare = Less<T>>
    class priority_queue
    {
    public:
        // 向上调整算法(插入时用)
        void AdjustUp(int child)
        {
            // 定义比较对象(用传入的仿函数)
            Compare com;

            // 父节点下标
            int parent = (child - 1) / 2;

            // 一直往上调整到根节点
            while (child > 0)
            {
                // 通过仿函数比较:
                // Less → parent < child → 交换 → 大堆
                if (com(_con[parent], _con[child]))
                {
                    // 交换父子节点
                    std::swap(_con[child], _con[parent]);

                    // 继续向上调整
                    child = parent;
                    parent = (child - 1) / 2;
                }
                else
                {
                    // 满足堆性质,停止调整
                    break;
                }
            }
        }

        // 入队:尾插 + 向上调整
        void push(const T& x)
        {
            _con.push_back(x);
            AdjustUp(_con.size() - 1);
        }

        // 向下调整算法(删除堆顶时用)
        void AdjustDown(int parent)
        {
            Compare com;
            size_t child = parent * 2 + 1; // 左孩子

            while (child < _con.size())
            {
                // 找出较大/较小的孩子(由仿函数决定)
                if (child + 1 < _con.size() 
                 && com(_con[child], _con[child + 1]))
                {
                    ++child;
                }

                // 父节点与孩子比较
                if (com(_con[parent], _con[child]))
                {
                    std::swap(_con[child], _con[parent]);
                    parent = child;
                    child = parent * 2 + 1;
                }
                else
                {
                    break;
                }
            }
        }

        // 出队(删除堆顶)
        void pop()
        {
            // 1. 堆顶与最后一个元素交换
            std::swap(_con[0], _con[_con.size() - 1]);

            // 2. 删除最后一个元素(原堆顶)
            _con.pop_back();

            // 3. 从根节点向下调整
            AdjustDown(0);
        }

        // 获取堆顶元素
        const T& top()
        {
            return _con[0];
        }

        // 获取元素个数
        size_t size() const
        {
            return _con.size();
        }

        // 判断是否为空
        bool empty() const
        {
            return _con.empty();
        }

    private:
        Container _con;  // 底层容器:默认vector
    };
}

核心知识点

  1. 整体结构: priority_queue 底层是堆; 默认是大顶堆; 底层容器默认用 vector

  2. 三个模板参数

cpp 复制代码
template<
    class T,
    class Container = vector<T>,
    class Compare = Less<T>
>

T:数据类型 Container:底层存储容器(vector) Compare:比较方式(仿函数)

  1. 大堆 / 小堆控制: Less<T> → 大顶堆(默认); Greater<T> → 小顶堆

  2. 核心算法

1.) push: 尾插; 对最后一个元素 AdjustUp 向上调整

2) pop: 堆顶与最后一个元素交换; 删除最后元素; 对根节点 AdjustDown 向下调整

  1. 为什么用仿函数?

把比较逻辑抽离; 不用改代码,只换仿函数就能切换大堆 / 小堆; 符合 STL 设计思想

test.cpp

cpp 复制代码
#include <iostream>
#include "priority_queue.h"
using namespace std;

int main()
{
	// 小顶堆
	gxy::priority_queue<int, vector<int>, Greater<int>> pq;

	pq.push(4);
	pq.push(1);
	pq.push(5);
	pq.push(7);
	pq.push(9);

	while (!pq.empty())
	{
		cout << pq.top() << " ";
		pq.pop();
	}
	cout << endl;

	return 0;
}

// 输出:1 4 5 7 9

大顶堆测试:

cpp 复制代码
// 大顶堆(默认 Less)
gxy::priority_queue<int> pq;

3.5 自定义类型入优先级队列

自定义类型需要重载 operator< 或 operator>,以提供比较逻辑:

cpp 复制代码
#include <iostream>
#include <queue>
#include <vector>
using namespace std;

// 日期类
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);
    }

    // 重载 << 用于打印日期
    friend ostream& operator<<(ostream& _cout, const Date& d) {
        _cout << d._year << "-" << d._month << "-" << d._day;
        return _cout;
    }

private:
    int _year;
    int _month;
    int _day;
};

// 测试:自定义类型放入优先级队列
void TestPriorityQueue() {
    // 大顶堆(默认 less,调用 operator<,最大的在堆顶)
    priority_queue<Date> q1;
    q1.push(Date(2018, 10, 29));
    q1.push(Date(2018, 10, 28));
    q1.push(Date(2018, 10, 30));

    // 堆顶是最大的日期
    cout << "大顶堆堆顶:" << q1.top() << endl; // 输出 2018-10-30

    // 小顶堆(使用 greater,调用 operator>,最小的在堆顶)
    priority_queue<Date, vector<Date>, greater<Date>> q2;
    q2.push(Date(2018, 10, 29));
    q2.push(Date(2018, 10, 28));
    q2.push(Date(2018, 10, 30));

    // 堆顶是最小的日期
    cout << "小顶堆堆顶:" << q2.top() << endl; // 输出 2018-10-28
}

int main() {
    TestPriorityQueue();
    return 0;
}

核心知识点

  1. 为什么自定义类型要重载运算符?

priority_queue 底层是堆,需要比较大小来调整结构; 自定义类型(如 Date)默认没有比较规则

所以必须提供: operator< 给 less 使用(大顶堆); operator> 给 greater 使用(小顶堆)

  1. 大顶堆 / 小顶堆原理

• priority_queue<Date>

默认:vector + less<Date>; 调用 Date::operator<; 结果:最大的元素在堆顶

• priority_queue<Date, vector<Date>, greater<Date>>

使用 greater<Date>; 调用 Date::operator>; 结果:最小的元素在堆顶

  1. 运行结果:大顶堆堆顶:2018-10-30 小顶堆堆顶:2018-10-28

3.6 经典例题:数组中第 K 个最大元素

用大顶堆找到数组中第 K 大的数:

cpp 复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        // 用所有元素构造大顶堆
        priority_queue<int> p(nums.begin(), nums.end());
        // 删除前k-1个最大元素
        for (int i = 0; i < k-1; ++i) {
            p.pop();
        }
        // 堆顶即为第k大元素
        return p.top();
    }
};

4. 容器适配器(Container Adapter)

4.1 什么是适配器

适配器是一种设计模式,将一个类的接口转换成用户期望的另一个接口。

例如:stack 和 queue 就是对底层容器的接口进行了封装,使其表现出栈和队列的行为。

4.2 STL 中 stack 和 queue 的默认底层容器

在 STL 标准库中,stack 和 queue 并非独立容器,而是容器适配器,它们默认使用 deque 作为底层容器:

cpp 复制代码
// stack 的模板定义
template <class T, class Container = deque<T>>
class stack;

// queue 的模板定义
template <class T, class Container = deque<T>>
class queue;

// priority_queue 的模板定义
template <class T, class Container = vector<T>, class Compare = less<typename Container::value_type>>
class priority_queue;

4.3 deque(双端队列)简介

• 双开口:可以在头尾两端进行 O(1) 复杂度的插入和删除操作。

• 底层结构:并非真正连续,而是由一段段小空间拼接而成,通过中控器(map)管理,对外表现为连续空间。

• 优缺点:

优点:头尾操作效率高,扩容时不需要搬移大量数据。

缺点:不适合遍历,因为迭代器需要频繁检测段边界,导致效率低下。

4.4 为什么选择 deque 作为 stack 和 queue 的默认底层容器

  1. 无需遍历:stack 和 queue 不需要迭代器,完美避开了 deque 遍历效率低的缺陷。

  2. 效率更高:

对于 stack:deque 在扩容时比 vector 更高效,不需要搬移大量数据。

对于 queue:deque 不仅效率高,而且内存利用率也比 list 更高。

4.5 STL 标准库中 stack 和 queue 的模拟实现

4.5.1 stack 的模拟实现

stack.h

cpp 复制代码
#pragma once    // 防止头文件被重复包含
#include <deque> // 默认底层容器使用 deque

namespace gxy
{
    // 栈的模拟实现 ------ 容器适配器
    // 第二个模板参数 Container:底层存储的容器
    // 默认使用 deque<T>,也可以传 vector、list
    template<class T, class Container = deque<T>>
    class stack
    {
    public:
        // 入栈:尾插
        void push(const T& x)
        {
            // 底层容器调用尾插,适配成栈的push
            _con.push_back(x);
        }

        // 出栈:尾删
        void pop()
        {
            // 底层容器调用尾删,适配成栈的pop
            _con.pop_back();
        }

        // 获取栈顶元素(只读)
        const T& top() const
        {
            // 栈顶 = 底层容器的最后一个元素
            return _con.back();
        }

        // 获取栈中元素个数
        size_t size() const
        {
            return _con.size();
        }

        // 判断栈是否为空
        bool empty() const
        {
            return _con.empty();
        }

    private:
        // 底层存储的容器(适配器的核心:不自己实现结构,复用容器)
        Container _con;
    };
}

4.5.2 queue 的模拟实现

queue.h

cpp 复制代码
#pragma once        // 防止头文件重复包含
#include <deque>    // 默认底层容器使用 deque

namespace gxy
{
    // 队列的模拟实现 ------ 容器适配器
    // 底层容器默认使用 deque<T>
    template<class T, class Container = deque<T>>
    class queue
    {
    public:
        // 入队:尾插
        void push(const T& x)
        {
            _con.push_back(x);
        }

        // 出队:头删
        void pop()
        {
            _con.pop_front();
        }

        // 获取队头元素(只读)
        const T& front() const
        {
            return _con.front();
        }

        // 获取队尾元素(只读)
        const T& back() const
        {
            return _con.back();
        }

        // 获取元素个数
        size_t size() const
        {
            return _con.size();
        }

        // 判断队列是否为空
        bool empty() const
        {
            return _con.empty();
        }

    private:
        Container _con;  // 底层存储容器
    };
}

测试:test.cpp

cpp 复制代码
#include<iostream>
#include<vector>
#include<list>
#include<stack>
#include<queue>
#include<algorithm>
using namespace std;

#include"Stack.h"
#include"Queue.h"

// 测试 stack / queue 容器适配器(支持切换底层容器)
int main()
{
    // ------------------- stack 测试 -------------------
    // 底层容器可以自由切换:
    // gxy::stack<int, vector<int>> st;
    // gxy::stack<int, list<int>> st;

    // 默认底层容器是 deque<int>
    gxy::stack<int, vector<int>> st;

    // 类模板特点:按需实例化
    // 用到哪个成员函数,才实例化哪个,不会全部生成
    st.push(1);
    st.push(2);
    st.push(3);
    st.push(4);

    // 获取栈顶 + 出栈
    cout << st.top() << endl;  // 输出 4
    st.pop();

    // ------------------- queue 测试 -------------------
    // gxy::queue<int, list<int>> q;  // 底层可以用 list
    gxy::queue<int> q;                // 默认底层用 deque

    q.push(1);
    q.push(2);
    q.push(3);
    q.push(4);

    // 获取队头 + 出队
    cout << q.front() << endl;  // 输出 1
    q.pop();

    return 0;
}

核心知识点

  1. 容器适配器可以自由切换底层容器

stack 底层支持: vector, list, deque(默认)

queue 底层支持:list,deque(默认),不支持 vector(因为 vector 没有 pop_front,效率极低)

  1. 类模板的重要特性:按需实例化

并不是把所有成员函数都编译出来; 你调用了哪个成员函数,才实例化哪个; 没用到的函数,不会生成代码,更不会报错

4.6 测试:vector 与 deque 排序效率对比

cpp 复制代码
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>
#include <ctime>
#include <cstdlib>

using namespace std;

void test_op1()
{
    // 设置随机数种子
    srand(time(0));
    const int N = 1000000;

    deque<int> dq;
    vector<int> v;

    // 同时给 vector 和 deque 插入 100 万随机数
    for (int i = 0; i < N; ++i)
    {
        auto e = rand() + i;
        v.push_back(e);
        dq.push_back(e);
    }

    // 排序 vector 并计时
    int begin1 = clock();
    sort(v.begin(), v.end());
    int end1 = clock();

    // 排序 deque 并计时
    int begin2 = clock();
    sort(dq.begin(), dq.end());
    int end2 = clock();

    // 打印耗时(单位:ms)
    printf("vector sort:%d\n", end1 - begin1);
    printf("deque  sort:%d\n", end2 - begin2);
}

运行结果(典型值):vector sort:31 deque sort:58

核心结论:

  1. vector 的排序速度明显比 deque 快

  2. 原因:内存结构差异

vector 是连续内存; 缓存命中率高; 迭代器就是原生指针,访问极快

deque 是分段连续内存;一段一段存储;访问时需要计算偏移、跨段;缓存命中率低,sort 会慢很多

  1. 所以:需要频繁 sort、随机访问 → 优先用 vector;需要频繁头尾插删 → 用 deque
cpp 复制代码
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>
#include <ctime>
#include <cstdlib>

using namespace std;

void test_op2()
{
    srand(time(0));
    const int N = 1000000;

    deque<int> dq1;
    deque<int> dq2;

    // 两个 deque 插入一模一样的数据
    for (int i = 0; i < N; ++i)
    {
        auto e = rand() + i;
        dq1.push_back(e);
        dq2.push_back(e);
    }

    // 方案1:直接对 deque 排序
    int begin1 = clock();
    sort(dq1.begin(), dq1.end());
    int end1 = clock();

    // 方案2:deque → 拷贝到 vector → 排序 → 拷贝回 deque
    int begin2 = clock();
    vector<int> v(dq2.begin(), dq2.end());
    sort(v.begin(), v.end());
    dq2.assign(v.begin(), v.end());
    int end2 = clock();

    printf("deque sort:%d\n", end1 - begin1);
    printf("deque copy vector sort, copy back deque:%d\n", end2 - begin2);
}

核心思路

1)对比两种给 deque 排序 的方式:

  1. 直接 sort(dq.begin(), dq.end())

  2. deque → 拷贝到 vector → 排序 → 拷回 deque

2)为什么要这么做?

deque 是分段连续内存,访问慢、缓存命中率低;vector 是连续内存,sort 速度快很多;即使多了两次拷贝,整体速度仍然可能更快

3)典型运行结果(直观感受):deque sort:58 deque copy vector sort, copy back deque:35

结论:对 deque 排序最优做法:拷贝到 vector 排序,再拷回 deque;哪怕多两次拷贝,整体效率也比直接 sort deque 更高。

5. 核心知识点总结

|----------------|------------|----------|-------------------------------|
| 数据结构 | 特性 | 底层容器(默认) | 核心操作 |
| stack | 后进先出(LIFO) | deque | push(), pop(), top() |
| queue | 先进先出(FIFO) | deque | push(), pop(), front(),back() |
| priority_queue | 堆(默认大顶堆) | vector | push(), pop(), top() |

关键结论:

• stack 和 queue 是容器适配器,而非独立容器,它们限制了底层容器的接口,以实现特定的行为。

• deque 被选为默认底层容器,是因为它在头尾操作上的高效性,完美适配了 stack 和 queue 的需求,同时避开了其遍历效率低的缺点。

• priority_queue 本质是堆,通过在 vector 上维护堆结构实现,默认是大顶堆,可通过比较器改为小顶堆。

相关推荐
2501_918126912 小时前
stm32能刷什么程序?
linux·stm32·单片机·嵌入式硬件·学习
赵谨言2 小时前
基于Python的汽车CAN总线报文格式转换系统的设计与实现
大数据·开发语言·经验分享·笔记·python
第七序章2 小时前
【Linux学习笔记】git三板斧
linux·运维·服务器·笔记·git·学习
xhyu612 小时前
【学习笔记】推荐系统 (1.基础知识)
笔记·学习
坚持就完事了2 小时前
Python各种命名规则
开发语言·python
郝学胜-神的一滴2 小时前
Python中的del语句与垃圾回收机制深度解析
服务器·开发语言·网络·python·算法
bonnyandsky2 小时前
X86 RouterOS 7.18 设置笔记十一:ROS更新方法及更新后IPTV组播转单播失效的解决方法
网络·笔记
重生之后端学习2 小时前
17. 电话号码的字母组合
java·开发语言·数据结构·算法·深度优先
软件资深者2 小时前
2026 版初中几何辅助线教材 PDF|打印即提分,中考几何 “分水岭” 一键通关
学习·数学·pdf·教学·初中数学