网站建设佰金手指科捷一app拉新项目
一:需要模拟实现的函数
本博客暂实现以下内容
解释:本篇实现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,甚至是更大的对象,传值返回就有一次拷贝构造,代价就大了。
② 避免了随随便便修改堆顶的数据,
其余过于简单 不再赘述