文章目录
priority_queue 的底层是堆。
C++标准库中 直接使用 std::make_heap, std::push_heap, std::pop_heap 来实现 priority_queue ,代码如下:
#include <iostream>
#include <vector>
#include <algorithm> // 用于 std::make_heap, std::push_heap, std::pop_heap
template <typename T, typename Container = std::vector<T>>
class MyPriorityQueue {
private:
Container data; // 使用底层容器存储优先队列的元素
public:
// 默认构造函数
MyPriorityQueue() {}
// 构造函数,可以指定底层容器
MyPriorityQueue(const Container& c) : data(c) {
std::make_heap(data.begin(), data.end());
}
// 将元素添加到优先队列中
void push(const T& value) {
data.push_back(value);
std::push_heap(data.begin(), data.end());
}
// 弹出优先队列中的最大元素
void pop() {
if (!empty()) {
std::pop_heap(data.begin(), data.end());
data.pop_back();
} else {
throw std::runtime_error("Priority queue is empty.");
}
}
// 访问优先队列中的最大元素
T& top() {
if (!empty()) {
return data.front();
} else {
throw std::runtime_error("Priority queue is empty.");
}
}
// 检查优先队列是否为空
bool empty() const {
return data.empty();
}
// 返回优先队列的大小
size_t size() const {
return data.size();
}
};
int main() {
// 使用默认底层容器(std::vector)的示例
MyPriorityQueue<int> pq1;
pq1.push(3);
pq1.push(1);
pq1.push(4);
pq1.push(1);
std::cout << "Top element of pq1: " << pq1.top() << std::endl;
pq1.pop();
std::cout << "Priority queue pq1 size after pop: " << pq1.size() << std::endl;
// 使用 std::vector 作为底层容器的示例
std::vector<int> vec = {9, 5, 7, 2, 6};
MyPriorityQueue<int, std::vector<int>> pq2(vec);
std::cout << "Top element of pq2: " << pq2.top() << std::endl;
pq2.pop();
std::cout << "Priority queue pq2 size after pop: " << pq2.size() << std::endl;
return 0;
}
堆的工作原理
堆(Heap)是一种特殊的完全二叉树,它满足下面的性质:
结构性质: 堆是一个完全二叉树,这意味着除了最后一层外,每一层都是完全填满的,而最后一层的节点则尽可能地集中在左边。
堆性质: 在一个最大堆(Max Heap)中,每个节点的值都大于或等于其子节点的值,根节点的值是堆中的最大值。相反,在一个最小堆(Min Heap)中,每个节点的值都小于或等于其子节点的值,根节点的值是堆中的最小值。
堆的插入和删除
插入操作(Insertion): 插入新元素时,新元素首先被放置在树的最后一个位置,以保持完全二叉树的结构。然后,该元素会通过一个称为“上浮”(或“堆化”)的过程,与其父节点比较并交换位置(如果在最大堆中新元素比父节点大,或在最小堆中新元素比父节点小)。这个过程重复进行,直到新元素到达一个位置,它不再比父节点大(或小,取决于是最大堆还是最小堆),或者它已经到达了树的顶部。
删除操作(Deletion): 在堆中,删除操作通常指的是删除根节点,即最大元素或最小元素。删除后,堆的结构性质必须得到维护。这通常通过将最后一个元素移到根节点的位置来完成,接着执行“下沉”(或“堆化”)过程,该元素会与其子节点比较并根据需要与较大(或较小)的子节点交换位置。这个过程持续进行,直到该元素位于正确的位置,或者它已经到达了树的底部。
构建堆(Heapify): 从无序数组构建堆的过程称为堆化(Heapify)。这可以通过从最后一个非叶子节点开始,对每个节点执行下沉操作来完成。在数组中,给定索引为i的元素,其左子节点的索引为2*i + 1,右子节点的索引为2*i + 2,父节点的索引为(i-1)/2。
如何用数组表示堆?
图解中的堆都是小根堆
上面的图片展示了对和数组的对应关系, 假定数组索引从1开始, 每个堆节点对应的数组索引用黑色方框在下面标注了, 可以看到, 堆的每个节点与数组之前的对应关系为:
给定索引为i的元素,其左子节点的索引为2*i + 1,右子节点的索引为2*i + 2,父节点的索引为(i-1)/2, 其父节点的索引为i//2
图解: 堆的插入
上面的图片展示了堆的插入过程, 蓝色节点表示新插入的元素, 首先将其插入在堆底, 然后和父节点进行比较, 如果新的节点更小, 就交换父子节点, 一直递归地进行直到根节点或父节点小于子节点
图解: 堆顶的删除
上面的图片展示了堆顶的删除过程, 删除时用堆底节点进行替换, 然后将根顶节点逐步下游, 注意下游时要选择更小的孩子进行交换
如果我们想自己实现堆,不用标准库里的堆函数,可以这么写,相对复杂一些(就是自己实现堆)
题目描述
C++ 代码实现
#include <iostream>
#include <vector>
#include <deque>
#include <stdexcept>
#include <sstream>
#include <string>
using namespace std;
template <typename T, typename Container = std::vector<T>>
class MyPriorityQueue {
private:
Container data;
// 辅助函数:向上调整元素以维护堆性质
void heapifyUp() {
int idx = data.size() - 1;
while(idx > 0) {
int parentIdx = (idx - 1) / 2;
if(data[idx] > data[parentIdx]) {
swap(data[idx], data[parentIdx]);
idx = parentIdx;
} else {
break;
}
}
}
// 辅助函数:向下调整元素以维护堆性质
void heapifyDown(Container& heap, int low, int high) {
int i = low;
int j = 2 * i + 1;
while(j <= high) {
if(j + 1 <= high && heap[j + 1] > heap[j]) {
j = j + 1;
}
if(heap[j] > heap[i]) {
swap(heap[j], heap[i]);
i = j;
j = 2 * i + 1;
} else {
break;
}
}
}
public:
MyPriorityQueue() {}
MyPriorityQueue(const Container& c) : data(c) {
int n = data.size();
for(int i = (n / 2) - 1; i >= 0; --i) {
heapifyDown(data, i, n - 1);
}
}
void push(const T& value) {
data.push_back(value);
heapifyUp();
}
void pop() {
int n = data.size();
if(!empty()) {
swap(data[0], data[n - 1]);
data.pop_back();
heapifyDown(data, 0, n - 2);
} else {
throw std::runtime_error("Priority queue is empty.");
}
}
T& top() {
if(!empty()) {
return data[0];
} else {
throw std::runtime_error("Priority queue is empty.");
}
}
bool empty() const {
return data.empty();
}
size_t size() const {
return data.size();
}
};
int main() {
// 使用 std::vector 作为底层容器
MyPriorityQueue<int, std::vector<int>> myPriorityQueue;
int N;
cin>> N;
getchar();
string line;
for(int i = 0; i < N; i++) {
getline(cin, line);
istringstream iss(line);
string command;
iss >> command;
int element;
if(command == "push") {
iss >> element;
myPriorityQueue.push(element);
}
if(command == "pop") {
try{
myPriorityQueue.pop();
} catch(const runtime_error& e) {
continue;
}
}
if (command == "top") {
try {
std::cout << myPriorityQueue.top() << std::endl;
} catch(const std::runtime_error& e) {
std::cout << "null" << std::endl;
}
}
if (command == "size") {
std::cout << myPriorityQueue.size() << std::endl;
}
if (command == "empty") {
std::cout << (myPriorityQueue.empty() ? "true" : "false") << std::endl;
}
}
return 0;
}
与C++标准库的区别:
STL中的std::priority_queue默认使用std::vector作为底层容器。这使得代码更加灵活,可以选择不同的容器类型。
代码中没有使用STL的std::make_heap、std::push_heap和std::pop_heap等堆算法来维护堆性质。相反,它使用了手动实现的堆操作函数heapifyUp和heapifyDown来维护堆。
代码中使用了自定义的异常处理来处理空队列的情况,而STL中的std::priority_queue可能会抛出std::out_of_range异常。
高频面试题
如何从给定的无序数组中找到第 K 大的元素?
215. 数组中的第K个最大元素【中等】(priority_queue)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<>> minHeap;
for(auto num: nums) {
minHeap.push(num);
if(minHeap.size() > k) {
minHeap.pop();
}
}
return minHeap.top();
}
};
实现一个优先队列,并解释它的插入和删除操作的时间复杂度。
什么是堆排序?它的时间复杂度和空间复杂度是多少?
二叉堆和斐波那契堆有什么区别?它们的操作的时间复杂度有何不同?
如何在 O(n) 时间复杂度内构建一个堆?
解释堆排序算法以及它的时间复杂度。
给定一个 k 有序数组,如何在最优时间复杂度内对它进行排序?
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!