堆与优先级队列
1. 二叉堆
1.1 二叉堆的定义
二叉堆在逻辑上是一颗完全二叉树,而在存储方式上还是用数组进行存储的。二叉堆具有如下性质,如果当前节点在数组中的索引为 ,那么有:
- 其左子节点在数组中的索引为 ;
- 其右子节点在数组中的索引为 ;
- 其父节点在数组中的索引为 ;
二叉堆的结构如下图所示:

常见的二叉堆有两种形式,分别是【大根堆】和【小根堆】,其中前者的堆顶是整个序列中最大的元素,后者的堆顶则是整个序列中最小的元素。如果下标 i 满足 ,其中 n 表示最后一个元素的下标,那么可以通过如下方法来判断当前序列为大根堆还是小根堆:
- 大根堆:满足 arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2]
- 小根堆:满足 arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2]
大根堆和小根堆如下图所示:

1.2 二叉堆的操作
二叉堆的操作分为入堆和出堆。入堆或者出堆后需要重新调整堆的结构,使其满足大根堆或者小根堆的条件,这个操作的过程分为【上浮】和【下沉】。
1.2.1 上浮
如图所示,为大根堆入堆后的上浮操作过程,先将入堆元素添加在数组末尾位置,然后与其父节点进行比较。对于大根堆而言,如果大于父节点,则与父节点交换位置;对于小根堆而言,如果小于父节点,则与父节点交换位置,重复上述操作,直到满足大根堆/小根堆的条件:

1.2.2 下沉
如图所示,为大根堆出堆时的下沉操作过程,先将堆顶元素出堆,然后将数组最末尾位置的元素放入到堆顶位置,与其左、右子节点进行比较。对于大根堆而言,找出值最大的子节点并与其交换位置;对于小根堆而言,找出值最小的子节点并与其交换位置,重复上述操作,直到其满足大根堆/小根堆的条件:

综上所述,无论是上浮操作还是下沉操作,其时间复杂度均为 ,空间复杂度均为 。
1.3 优先级队列的实现
优先级队列的代码实现如下,可以通过传入不同的函数对象来创建大根堆/小根堆,当传入的函数对象为 greater<int>()
时为大根堆,为 less<int>()
时为小根堆:
class PriorityQueue { public: using Comp = function<bool(int, int)>; PriorityQueue(int cap = 20, Comp comp = greater<int>()); ~PriorityQueue(); // 入堆 void push(int val); // 出堆 void pop(); // 获取堆顶元素 int top() const; // 堆大小 int size() const; // 堆是否为空 bool empty() const; // 打印堆 void print() const; private: // 上浮操作 void siftUp(int idx, int val); // 下沉操作 void siftDown(int idx, int val); // 数组扩容 void expand(int size); private: int* que_; // 动态扩容的数组 int size_; // 数组元素的个数 int cap_; // 数组的总空间大小 Comp comp_; // 比较器对象 }; PriorityQueue::PriorityQueue(int cap, Comp comp) : que_(new int[cap]) , size_(0) , cap_(cap) , comp_(comp) { } PriorityQueue::~PriorityQueue() { delete[] que_; que_ = nullptr; } void PriorityQueue::push(int val) { if (size_ == cap_) expand(cap_ * 2); if (size_ == 0) { que_[size_] = val; } else { siftUp(size_, val); } size_++; } void PriorityQueue::pop() { if (size_ == 0) throw "priority queue is empty"; size_--; if (size_ != 0) { siftDown(0, que_[size_]); } } int PriorityQueue::top() const { if (size_ == 0) throw "priority queue is empty"; return que_[0]; } int PriorityQueue::size() const { return size_; } bool PriorityQueue::empty() const { return size_ == 0; } void PriorityQueue::print() const { for (int i = 0; i < size_; i++) { cout << que_[i] << " "; } cout << endl; } void PriorityQueue::siftUp(int idx, int val) { while (idx > 0) { int father = (idx - 1) / 2; if (comp_(val, que_[father])) { que_[idx] = que_[father]; idx = father; } else { break; } } que_[idx] = val; } void PriorityQueue::siftDown(int idx, int val) { // 下沉时不能超过最后一个有孩子的节点 while (idx < size_ / 2) { int child = 2 * idx + 1; if (child + 1 < size_ && comp_(que_[child + 1], que_[child])) { // 有右孩子 且右孩子的值大于左孩子 则将其更新为右孩子 child++; } if (comp_(que_[child], val)) { que_[idx] = que_[child]; idx = child; } else { break; } } que_[idx] = val; } void PriorityQueue::expand(int size) { int* p = new int[size]; memcpy(p, que_, cap_ * sizeof(int)); delete[] que_; que_ = p; cap_ = size; }
2. STL实现
2.1 heap
heap 并不属于 STL 的容器组件,它是 priority queue 的底层容器,实现了二叉堆数据结构。priority queue 允许用户以任何次序将任何元素推入容器内,但取出时一定是从优先级最高的元素开始取,因此称作【优先级队列】。
heap 的实现是通过一个完全二叉树,即整棵二叉树除了最底层的叶节点之外,是填满的,而最底层的叶节点由左至右又不得有空隙。
SGI STL 中将此数组以 vector 来代替。根据元素排列方式,heap 可以分为【max-heap(大根堆)】和【min-heap(小根堆)】两种,前者每个节点的键值都大于或等于其子节点键值,后者的每个节点键值都小于或等于其子节点键值。因此,前者的最大值在根节点,并位于数组的起始处,而后者的最小值在根节点,并位于数组的起始处。STL 提供的是 max-heap。
heap 所有元素都必须遵循特别的完全二叉树排列规则,所以 heap 不提供遍历功能,也不提供迭代器。
2.2 priority queue
priority queue 是一个拥有权值观念的 queue,它允许加入新元素,移除旧元素,审视元素值等功能。由于这是一个 queue,所以只允许在底端加入元素,并从顶端取出元素,除此之外别无其他存取元素的途径。
priority queue 带有权值观念,其内的元素并非按照被推入的次序排列,而是自动依照元素的权值排列。权值最高者,排在最前面。
缺省情况下,priority queue 利用一个 max-heap 完成。

注意:pop()
并不是真的将元素弹出,而是重排 heap,然后再以底层容器的 pop_back()
取得被弹出的元素。priority queue 不提供遍历功能,也不提供迭代器。
本文作者:Leaos
本文链接:https://www.cnblogs.com/tuilk/p/16997517.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步