priority_queue的模拟实现
一:需要模拟实现的函数
本博客暂实现以下内容
解释:本篇实现priority_queue的重点在于
①:向上/向下调整函数
优先级队列相较于普通的队列,其区别主要是在 push 和 pop 上,即需要在插入 / 删除数据的同时,增添调整的功能,已达到堆的效果
②:priority_queue是一个适配器类的体现
③:仿函数结合向上/向下调整函数,以达到不修改向上/向下调整函数内部即可分别实现大小堆
仿函数不太懂的先看这篇:仿函数 VS 函数指针实现回调 -CSDN博客
堆排序都不懂的先看这篇:堆排序 _7-3 堆排序-CSDN博客
向上/向下调整复杂度看这篇:向上or向下调整建堆 的时间复杂度_向上调整法的时间复杂度-CSDN博客
二:代码
namespace bit
{
//仿函数
// less: 小于的比较
template<class T>
struct less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
//仿函数
// greater: 大于的比较
template<class T>
struct greater
{
bool operator()(const T& x, const T& y) const
{
return x > y;
}
};
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
public:
// 构造函数1:构造空的优先级队列
priority_queue()
: _con()
{}
// 构造函数2:迭代器区间构造优先级队列
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
: _con(first, last)
{
int count = _con.size();
int root = ((count - 2) / 2);
for (; root >= 0; root--) {
AdjustDown(root);
}
}
// 向上调整算法
void AdjustUp(size_t child) {
Compare cmpFunc;
size_t father = (child - 1) / 2;
while (child > 0)
{
if (cmpFunc(_con[father], _con[child])) {
swap(_con[father], _con[child]);
child = father;
father = (child - 1) / 2;
}
else
{
break;
}
}
}
// 向下调整算法
void AdjustDown(size_t father)
{
Compare cmpFunc;
size_t child = father * 2 + 1; // 默认认为左孩子大
while (child < _con.size())
{
if (child + 1 < _con.size() && cmpFunc(_con[child], _con[child + 1]))
{
child += 1;
}
if (cmpFunc(_con[father], _con[child]))
{
swap(_con[father], _con[child]);
father = child;
child = father * 2 + 1;
}
else
{
break;
}
}
}
/* 入数据 */
void push(const T& x)
{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
/* 出数据 */
void pop()
{
assert(!_con.empty());
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
/* 返回堆顶数据 */
const T& top()
{
return _con[0];
}
/* 返回大小 */
size_t size()
{
return _con.size();
}
/* 判断是否为空 */
bool empty()
{
return _con.empty();
}
private:
Container _con;//一个容器类的对象
};
// 小于比较,搞大堆
void test_priority_queue1()
{
priority_queue<int> pq;
pq.push(2);
pq.push(5);
pq.push(1);
pq.push(6);
pq.push(8);
pq.push(9);
pq.push(3);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
// 大于比较,搞小堆
void test_priority_queue2()
{
priority_queue<int, vector<int>, greater<int>> pq;
pq.push(2);
pq.push(5);
pq.push(1);
pq.push(6);
pq.push(8);
pq.push(9);
pq.push(3);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
}
int main(void)
{
bit::test_priority_queue1();
bit::test_priority_queue2();
return 0;
}
运行结果:
三:代码解释
1. 仿函数(Functor)
仿函数是一个类,重载了 operator()
,使得它的实例可以像函数一样被调用。代码中定义了两个仿函数:
less<T>
:用于比较两个值的大小,返回 x < y
的结果。默认情况下,它用于构建大堆(堆顶元素最大)。
greater<T>
:用于比较两个值的大小,返回 x > y
的结果。它用于构建小堆(堆顶元素最小)。
//仿函数
// less: 小于的比较
template<class T>
struct less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
//仿函数
// greater: 大于的比较
template<class T>
struct greater
{
bool operator()(const T& x, const T& y) const
{
return x > y;
}
};
2. 优先级队列类 priority_queue
你的 priority_queue
类是一个模板类,支持以下功能:
模板参数
T
:队列中存储的元素类型。
Container
:底层容器类型,默认是 std::vector<T>
。
Compare
:比较函数对象类型,默认是 less<T>
(用于构建大堆)
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue {
// ...
};
成员变量
_con
:底层容器,用于存储堆中的元素。
注意:_cmpFunc
并不算成员变量,他只是在调整函数中创建的临时对象,用于实现大于比较和小于比较
构造函数
默认构造函数:创建一个空的优先级队列。
范围构造函数:通过迭代器范围初始化队列,并调用 AdjustDown
构建堆。
提示:范围构造函数还能这样写:
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
: _con(first, last)
{
int count = _con.size();
int root = ((count - 2) >> 1);
for (; root >= 0; root--) {
AdjustDown(root);
}
}
解释:
/ 2
可以替换成 >> 1
,因为对于正整数来说,右移一位(>> 1
)和除以 2(/ 2
)的结果是相同的。两者都是将数值除以 2 并向下取整。
堆调整算法
AdjustUp
AdjustUp作用:
是将新插入的元素从底部逐步向上调整,直到整个堆重新满足堆的性质(大堆或小堆)。
AdjustUp代码解释:
2. 参数说明
-
child
:当前需要调整的节点的索引(通常是新插入元素的索引)。
3. 核心逻辑
(1)计算父节点索引
-
在完全二叉树中,父节点和子节点的索引关系为:
-
父节点索引:
father = (child - 1) / 2
。 -
左孩子索引:
child = father * 2 + 1
。 -
右孩子索引:
child = father * 2 + 2
。
因此,
father = (child - 1) / 2
用于计算当前节点的父节点索引。 -
(2)比较父节点和当前节点
-
使用仿函数
cmpFunc
比较父节点和当前节点:-
如果
cmpFunc(_con[father], _con[child])
返回true
,表示父节点不满足堆的性质,需要交换。 -
如果返回
false
,表示父节点已经满足堆的性质,调整结束。
-
(3)交换节点
-
如果父节点不满足堆的性质,交换父节点和当前节点的值。
-
更新当前节点为父节点,继续向上调整。
(4)终止条件
-
当
child
为 0(即当前节点已经是根节点)时,调整结束。 -
如果父节点已经满足堆的性质,调整结束。
AdjustDown
:
AdjustDown作用:
是将一个元素删除之后,对于剩下元素从根节点逐步向下调整,直到整个堆重新满足堆的性质(大堆或小堆)。(规定:删除指的是删除头结点 要先交换头尾元素 然后缩小堆范围1 再从头向下调整)
AdjustDown
代码解释:
2. 参数说明
-
father
:当前需要调整的节点的索引(通常是堆顶元素的索引)。
3. 核心逻辑
(1)初始化
-
child = father * 2 + 1
:计算当前节点的左孩子索引。 -
默认认为左孩子是较大的孩子。
(2)选择较大的孩子
-
如果右孩子存在(
child + 1 < _con.size()
)且右孩子比左孩子大(cmpFunc(_con[child], _con[child + 1])
),则将child
更新为右孩子的索引。 -
这样做的目的是确保
child
指向当前节点的较大孩子。
(3)比较父节点和较大的孩子
-
使用仿函数
cmpFunc
比较父节点和较大的孩子:-
如果
cmpFunc(_con[father], _con[child])
返回true
,表示父节点不满足堆的性质,需要交换。 -
如果返回
false
,表示父节点已经满足堆的性质,调整结束。
-
(4)交换节点
-
如果父节点不满足堆的性质,交换父节点和较大的孩子的值。
-
更新父节点为较大的孩子,继续向下调整。
(5)终止条件
-
当
child
超出堆的范围(child >= _con.size()
)时,调整结束。 -
如果父节点已经满足堆的性质,调整结束。
(6)代码需要注意的细节:
1:向下调整的极限是child位于最后一个元素,即下标达到最大值n-1,(n就是size)(比如6个元素,下标最大只能取到5),所以child只能小于n,不可能大于或等于n
2:但是在确定孩子中较小值的时候,会用到a[child+1,]来判断,所以为了避免超出范围,确定范围的这个if应该是&&,左面确保child+1<n,左面成立,才会执行&&右面的比较,否则会越界访问
3:不能为了将就第一个if,而在while的条件直接 child<n-1,因为如果child的下标会更改为最后一个元素,此时child 的值为n-1,此时会因为错误的判断,而不进入循环判断,应该是先进入循环,不进去刚才的if,最后再让那一个孩子和他比较。
其余函数
push:插入一个新元素,并调用 AdjustUp 维护堆的性质。
pop:删除堆顶元素,并调用 AdjustDown 维护堆的性质。
top:返回堆顶元素。
size:返回队列中元素的数量。
empty:判断队列是否为空。
解释:
值得注意的是,返回堆顶数据的 top 接口,我们用了 const 引用返回。
Q:为什么这里要用引用?为什么还要加个 const?
① 考虑到如果这里 T 是 string,甚至是更大的对象,传值返回就有一次拷贝构造,代价就大了。
② 避免了随随便便修改堆顶的数据,
其余过于简单 不再赘述