一、仿函数(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)
的返回值决定是否需要交换。例如,大顶堆中com
是less
,当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 的核心功能,其设计特点包括:
-
适配器模式:基于底层容器(如 vector)实现,复用容器的存储能力,专注于堆的逻辑维护。
-
策略模式:通过仿函数
Compare
灵活切换比较规则,轻松支持大顶堆 / 小顶堆。 -
高效操作:插入(
push
)和删除(pop
)均为 O (logn) 时间复杂度,获取堆顶(top
)为 O (1),适合需要频繁获取最值的场景(如任务调度、贪心算法等)。
理解 priority_queue 的关键在于掌握堆的调整算法(向上 / 向下调整),以及仿函数如何影响堆的性质。