优先级队列的简单模拟实现
优先级队列的简单模拟实现
chengzi什么是优先级队列
在 C++ 中,std::priority_queue 是一个优先级队列的实现,它提供了按照优先级进行元素存储和访问的功能。与 Python 中的 PriorityQueue 类似,C++ 的 std::priority_queue 使用
堆数据结构来实现。
创建一个优先级队列:
1  | 
  | 
添加元素到队列:
1  | pq.push(5); // 添加 5 到队列  | 
获取并移除最高优先级的元素:
1  | int topElement = pq.top(); // 获取最高优先级的元素(不移除)  | 
检查队列是否为空:
1  | bool isEmpty = pq.empty(); // 检查队列是否为空  | 
返回队列中元素的个数:
1  | int size = pq.size(); // 返回队列中元素的个数  | 
默认情况下,std::priority_queue 的元素是按照
降序排列的。也就是说,最大的元素拥有最高的优先级。如果想要实现最小值优先的队列,可以传递一个比较函数对象作为模板参数。
堆(预备知识)
什么是堆?称为堆的前提得是一棵完全二叉树,且堆中某个结点的值总是不大于或不小于其父结点的值。
类型
最大堆(大根堆):根结点最大的堆。
最小堆(小根堆):根结点最小的堆。
[图片生成自'https://algo.hufeifei.cn/Heap.html']如图是一个小根堆。
堆的存储方式采用顺序存储(常用数组),从上到下,从左到右,这样的存储方式非常适合找到父节点或者孩子节点,其符合规律:
任何一个节点*2就能得到它的左孩子下标,再+1就能得到它的右孩子下标(前提是孩子都存在)。
任何一个节点除2再向下取整就能得到父节点下标。
使用该规律前提是下标从1开始!
如何建堆?
总结为:倒着建。从数组的最后往前走,每一个叶子结点都是一个独立的小堆,那么直接从最后一个非叶结点开始调整。为什么要倒着来?因为每检查到一个节点,它的子节点包括子节点的子节点都已经调整完毕了(随着父节点变动可能子节点还会有调整,在代码部分细讲)。
图中的最后一个节点是145,不难看出它的父节点12就是最后一个非叶结点。如果12的左子节点比它还大,就该进行调整,右子节点比他小也该进行调整。调整的准则就是目前节点为根的堆必须满足是大根堆或者小根堆。当这样一轮调整结束,最大(最小)的那个节点就被成功移到了堆顶。
构建最大堆:将数组重新组织成一个最大堆。
建堆过程
将堆顶元素(最大值)与堆的最后一个元素交换。
减小堆的大小(排除已经排好序的最大元素)。
重新调整堆,使其满足最大堆性质。
重复上述步骤,直到堆的大小为1。
对数组 [3, 7, 2, 4, 1] 构建大根堆:
初始数组对应的完全二叉树表示
1  | 3  | 
先计算最后一个非叶子节点的索引,对于长度为 n = 5 的数组,最后一个非叶子节点索引是 n / 2 - 1 = 1(对应元素 7 这个节点)。
从最后一个非叶子节点(索引为1,值为7)开始调整
- 节点 
7的左子节点是4,右子节点是1,因为7大于4和1,满足大根堆性质,无需调整。此时状态依然是:1
2
3
4
53
/ \
7 2
/ \ /
4 1 - 接着处理索引为 
0的节点(值为3),它的左子节点是7,右子节点是2。比较3与7和2,发现7最大,所以交换3和7的位置,得到:1
2
3
4
57
/ \
3 2
/ \ /
4 1 - 交换后,以 
3为根的子树可能不满足大根堆性质了,继续检查调整。此时3的左子节点4大于3,所以再次交换3和4,得到:1
2
3
4
57
/ \
4 2
/ \ /
3 1 
此时整个二叉树就满足大根堆性质了,对应的数组 [7, 4, 2, 3, 1] 就是构建好的大根堆。
如果要进行整个数组的排序,还要进行多次调整。目前只是把7作为最大的数筛选出来,而其他元素还没有完成排序。
交换堆顶元素和最后一个元素
- 初始大根堆对应的树状结构为:对应的数组为
1
2
3
4
57
/ \
4 2
/ \ /
3 1[7, 4, 2, 3, 1]。 - 交换堆顶元素 
7和最后一个元素1,交换后的树状结构变为:对应的数组变为1
2
3
4
51
/ \
4 2
/ \ /
3 7[1, 4, 2, 3, 7]。此时最大元素7已经在数组末尾位置了,后续要对前面的元素(除了已确定位置的7)重新调整为大根堆来继续确定次大元素等。 
调整剩余元素为大根堆
- 把交换后的数组中前面 
n - 1个元素(这里n = 5,即对前4个元素[1, 4, 2, 3])重新调整为大根堆。 - 从根节点(索引为 
0,值为1)开始,其左子节点是4,右子节点是2,比较发现4最大,交换1和4,得到:对应的数组变为1
2
3
4
54
/ \
1 2
/ \ /
3 7[4, 1, 2, 3, 7]。 - 接着以 
1为根节点检查调整,1的左子节点3大于1,交换1和3,得到:对应的数组变为1
2
3
4
54
/ \
3 2
/ \ /
1 7[4, 3, 2, 1, 7],此时前面4个元素又构成了大根堆。 
重复上述交换和调整步骤
- 再次交换堆顶元素(此时堆顶元素为 
4)和当前最后一个未确定位置的元素(即索引为3的元素1),交换后的树状结构为:对应的数组变为1
2
3
4
51
/ \
3 2
/ \ /
4 7[1, 3, 2, 4, 7]。 - 然后对前面 
3个元素([1, 3, 2])重新调整为大根堆,从根节点(索引为0,值为1)开始,其左子节点3大于1,交换1和3,得到:对应的数组变为1
2
3
4
53
/ \
1 2
/ \ /
4 7[3, 1, 2, 4, 7],此时1的左子节点4大于1,但右子节点2小于1,无需再交换,前面3个元素构成大根堆。 
继续重复
- 交换堆顶元素(此时堆顶元素为 
3)和当前最后一个未确定位置的元素(即索引为2的元素2),交换后的树状结构为:对应的数组变为1
2
3
4
52
/ \
1 3
/ \ /
4 7[2, 1, 3, 4, 7]。 - 对前面 
2个元素([2, 1]),因为2大于1,本身就满足大根堆性质。 
最后一次交换
- 交换堆顶元素(此时堆顶元素为 
2)和当前最后一个未确定位置的元素(即索引为1的元素1),交换后的树状结构为:对应的数组变为1
2
3
4
51
/ \
2 3
/ \ /
4 7[1, 2, 3, 4, 7],至此整个数组完成升序排序。 
总结:
- 首先将给定数组构建成大根堆。
 - 然后重复交换堆顶元素(最大元素)和当前未排序部分的最后一个元素,接着对交换后除去最后已确定位置元素的剩余元素重新调整为大根堆,不断循环这个交换和调整过程,直到所有元素都确定了最终位置,数组就完成了升序排序。
 
模拟实现
官方定义:
1  | template <  | 
T 是存储在优先级队列中的元素类型;Container 是用于存储元素的底层容器类型,默认是 std::vector
queue里可以塞入各种类型,所以比较大小还得借助模版参数来进行优化。
1  | template<class T>  | 
这样就完成了最大值排列和最小值排列。
建堆
1  | 
  | 


