【C++】 priority_queue 容器模拟实现解析

一、仿函数(Functor):比较策略的封装

仿函数是重载了()运算符的类 / 结构体,可像函数一样被调用。在 priority_queue 中,仿函数用于定义元素的比较规则,实现大顶堆 / 小顶堆的灵活切换。

cpp 复制代码
// 仿函数:小于比较(用于大顶堆,默认)
template<class T>
struct less
{
    bool operator()(const T& left, const T& right) const
    {
        return left < right;  // 左 < 右时返回true
    }
};

// 仿函数:大于比较(用于小顶堆)
template<class T>
struct greater
{
    bool operator()(const T& left, const T& right) const
    {
        return left > right;  // 左 > 右时返回true
    }
};

解析:

  • 仿函数的核心是operator(),它定义了两个元素的比较逻辑。

  • less<T>:当left < right时返回true,配合堆调整逻辑可实现 "大顶堆"(父节点 > 子节点)。

  • greater<T>:当left > right时返回true,配合堆调整逻辑可实现 "小顶堆"(父节点 < 子节点)。

  • 这种设计采用 "策略模式",让 priority_queue 无需修改核心代码,仅通过更换仿函数即可切换堆类型。

二、priority_queue 类的核心实现

priority_queue(优先队列)是一种 "适配器容器",它基于底层容器(默认 vector)实现,内部维护一个 "堆" 结构(完全二叉树),确保每次可高效获取优先级最高的元素(堆顶)。

2.1 模板参数与成员变量
cpp 复制代码
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
public:
    // 成员函数声明(见后续)
private:
    Container _con;  // 底层容器,用于存储堆的元素
};

解析:

  • 模板参数:

    • T:存储的元素类型。

    • Container:底层容器类型(默认 vector),需支持随机访问(如[]操作)和尾部插入 / 删除(push_back/pop_back),因此通常选 vector 或 deque(list 不适合,因不支持随机访问)。

    • Compare:比较仿函数(默认less<T>),决定堆的类型(大顶堆 / 小顶堆)。

  • 成员变量_con:底层容器实际存储元素,堆的结构通过该容器的元素顺序体现(完全二叉树的层次遍历顺序)。

2.2 堆的核心调整算法

堆的核心是 "向上调整" 和 "向下调整",用于在插入 / 删除元素后维持堆的性质(父节点优先级高于 / 低于子节点)。

2.2.1 向上调整(AdjustUp)

插入元素后,需将新元素从尾部上移到合适位置,确保堆性质。

cpp 复制代码
void AdjustUp(int child)
{
    Compare com;  // 实例化仿函数,获取比较规则
    int parent = (child - 1) / 2;  // 计算父节点索引(完全二叉树性质)
    
    while (child > 0)  // 子节点索引>0,说明未到根节点
    {
        // 若父节点不符合堆规则(需与子节点交换)
        // 大顶堆:父 < 子 → 交换;小顶堆:父 > 子 → 交换(由com决定)
        if (com(_con[parent], _con[child]))
        {
            swap(_con[child], _con[parent]);  // 交换父子节点
            child = parent;  // 子节点上移到父节点位置
            parent = (child - 1) / 2;  // 重新计算新的父节点
        }
        else
        {
            break;  // 已满足堆性质,退出循环
        }
    }
}

解析:

  • 适用场景:元素插入(push)后调用,新元素初始在容器尾部(child = 最后一个索引)。

  • 核心逻辑:通过(child - 1) / 2计算父节点索引,反复比较父子节点,若父节点不符合规则则交换,直到根节点或满足堆性质。

  • 仿函数作用:com(parent, child)的返回值决定是否需要交换。例如,大顶堆中comless,当parent < child时返回true,触发交换。

2.2.2 向下调整(AdjustDown)

删除堆顶元素后,需将新的根节点下移到合适位置,维持堆性质。

cpp 复制代码
void AdjustDown(int parent)
{
    Compare com;  // 实例化仿函数
    int child = parent * 2 + 1;  // 计算左子节点索引(完全二叉树性质)
    
    while (child < _con.size())  // 子节点索引有效(未超出容器范围)
    {
        // 找"更需要交换"的子节点(左/右子节点中优先级更高的)
        // 大顶堆:找更大的子节点;小顶堆:找更小的子节点(由com决定)
        if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
        {
            child++;  // 右子节点更符合交换条件,切换到右子
        }
        
        // 若父节点不符合堆规则(需与子节点交换)
        if (com(_con[parent], _con[child]))
        {
            swap(_con[child], _con[parent]);  // 交换父子节点
            parent = child;  // 父节点下移到子节点位置
            child = parent * 2 + 1;  // 重新计算新的左子节点
        }
        else
        {
            break;  // 已满足堆性质,退出循环
        }
    }
}

解析:

  • 适用场景:堆顶元素删除(pop)后调用,新根节点初始为原最后一个元素(parent = 0)。

  • 核心逻辑:

    • 先通过parent * 2 + 1计算左子节点,比较左 / 右子节点(若右子存在),选择 "更需要交换" 的子节点(由仿函数决定)。

    • 比较父节点与选中的子节点,若不符合规则则交换,反复下移直到叶子节点或满足堆性质。

  • 为什么先比较子节点?确保父节点与 "最优子节点" 交换,一次调整即可维持堆性质。

2.3 优先队列的核心接口
2.3.1 插入元素(push)
cpp 复制代码
void push(const T& x)
{
    _con.push_back(x);  // 先将元素插入到底层容器尾部
    AdjustUp(_con.size() - 1);  // 对新插入的元素(尾部)进行向上调整
}

解析:

  • 步骤:先在底层容器尾部添加元素(O (1) 时间,vector 的优势),再通过AdjustUp将其移动到合适位置(O (logn) 时间,n 为元素个数)。

  • 目的:确保插入后仍维持堆的性质。

2.3.2 删除堆顶元素(pop)
cpp 复制代码
void pop()
{
    if (empty())  // 空队列直接返回
        return;
    swap(_con[_con.size() - 1], _con[0]);  // 交换堆顶与最后一个元素
    _con.pop_back();  // 删除最后一个元素(原堆顶)
    AdjustDown(0);  // 对新堆顶(原最后一个元素)进行向下调整
}

解析:

  • 步骤:

    • 交换堆顶(索引 0)与最后一个元素(O (1));

    • 删除尾部元素(原堆顶,O (1));

    • 对新堆顶(原尾部元素)进行向下调整(O (logn))。

  • 为什么不直接删除堆顶?直接删除会导致底层容器出现 "空洞",破坏完全二叉树结构,而交换后删除尾部可高效维护结构。

2.3.3 其他基础接口
cpp 复制代码
T& top() { return _con[0]; }  // 获取堆顶元素(优先级最高的元素)
size_t size() { return _con.size(); }  // 返回元素个数
bool empty() { return _con.empty(); }  // 判断队列是否为空

解析:

  • top():堆顶元素始终在索引 0(完全二叉树的根),直接返回即可(O (1))。

  • 其他接口直接复用底层容器的功能,简洁高效。

三、测试函数与验证
cpp 复制代码
void priority_queue_test()
{
    // 1. 大顶堆(默认使用less仿函数)
    priority_queue<int> max_heap;
    max_heap.push(8);
    max_heap.push(18);
    max_heap.push(3);
    max_heap.push(6);
    max_heap.push(10);
    cout << "大顶堆弹出顺序:";
    while (!max_heap.empty())
    {
        cout << max_heap.top() << " ";  // 输出:18 10 8 6 3
        max_heap.pop();
    }
    cout << endl;

    // 2. 小顶堆(指定greater仿函数)
    priority_queue<int, vector<int>, greater<int>> min_heap;
    min_heap.push(8);
    min_heap.push(18);
    min_heap.push(3);
    min_heap.push(6);
    min_heap.push(10);
    cout << "小顶堆弹出顺序:";
    while (!min_heap.empty())
    {
        cout << min_heap.top() << " ";  // 输出:3 6 8 10 18
        min_heap.pop();
    }
    cout << endl;
}

解析:

  • 大顶堆测试:使用默认less仿函数,每次弹出最大元素(18 → 10 → 8 → 6 → 3),符合大顶堆性质。

  • 小顶堆测试:显式指定greater仿函数,每次弹出最小元素(3 → 6 → 8 → 10 → 18),符合小顶堆性质。

  • 测试结果验证了堆调整算法和仿函数的正确性:插入元素后堆结构正确,弹出顺序符合优先级规则。

总结

该实现完整模拟了 C++ 标准库中 priority_queue 的核心功能,其设计特点包括:

  1. 适配器模式:基于底层容器(如 vector)实现,复用容器的存储能力,专注于堆的逻辑维护。

  2. 策略模式:通过仿函数Compare灵活切换比较规则,轻松支持大顶堆 / 小顶堆。

  3. 高效操作:插入(push)和删除(pop)均为 O (logn) 时间复杂度,获取堆顶(top)为 O (1),适合需要频繁获取最值的场景(如任务调度、贪心算法等)。

理解 priority_queue 的关键在于掌握堆的调整算法(向上 / 向下调整),以及仿函数如何影响堆的性质。

相关推荐
ST.J3 小时前
swing笔记
java·笔记
jiaway3 小时前
【C语言】第四课 指针与内存管理
c语言·开发语言·算法
励志不掉头发的内向程序员3 小时前
C++进阶——继承 (1)
开发语言·c++·学习
做就对了66663 小时前
驱动员工的核心:少谈“大道理”,多解“人心”
职场和发展·职场·管理·团队管理·销售
四谎真好看4 小时前
Java 学习笔记(进阶篇2)
java·笔记·学习
mit6.8245 小时前
并查集|栈
c++
中国胖子风清扬5 小时前
Rust 序列化技术全解析:从基础到实战
开发语言·c++·spring boot·vscode·后端·中间件·rust
我就是全世界5 小时前
【存储选型终极指南】RustFS vs MinIO:5大维度深度对决,95%技术团队的选择秘密!
开发语言·分布式·rust·存储