本篇博客,我们介绍一下堆的相关概念,然后介绍一下C++中和Java中的优先级队列。
什么是堆?
堆是一种特殊的二叉树。
- 堆是一个完全二叉树;
- 堆采用顺序存储的方式;
- 堆中任一结点都都满足比子节点的值大(大堆)或小(小堆)。
我们来看一个大堆及其在内存中的存储方式:
从上图我们可以看出,父结点和子节点的下标关系如下:
已知父结点下标为x,则孩子结点下标为:
- 左孩子下标为: 2 ∗ x + 1 2*x+1 2∗x+1;
- 右孩子下标为: 2 ∗ x + 2 2*x + 2 2∗x+2;
已知孩子下标为x,则父结点下标为:
- x为左孩子下标,则父结点下标为: ( x − 1 ) / 2 (x - 1) / 2 (x−1)/2;
- x为右孩子下标,则父结点下标为: ( x − 1 ) / 2 (x - 1)/2 (x−1)/2或 ( x − 2 ) / 2 (x - 2)/2 (x−2)/2;
实现一个简单的堆
C语言版
heap.h:
#pragma once
typedef int ElementType;
/*---堆---*/
typedef struct Heap {
ElementType* heap;
size_t size;
size_t capacity;
} Heap;
/*---初始化---*/
void heapInit(Heap* h);
/*---释放---*/
void heapDestory(Heap* h);
/*---入堆---*/
void heapPush(Heap* h, ElementType x);
/*---出堆---*/
void heapPop(Heap* h);
/*---堆顶元素---*/
ElementType heapTop(const Heap* h);
/*---堆大小---*/
size_t heapSize(const Heap* h);
/*---空堆---*/
bool heapEmpty(const Heap* h);
/*---堆排序---*/
void heapSort(Heap* h);
/*---打印---*/
void heapDisplay(const Heap* h);
/*---堆测试---*/
void heapTest();
heap.c:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
#include "heap.h"
void heapInit(Heap* h){
if (h == NULL){
return;
}
h->capacity = 1;
h->size = 0;
h->heap = (ElementType*)malloc(sizeof(ElementType) * h->capacity);
assert(h->heap != NULL);
}
void heapDestory(Heap* h){
if (h == NULL){
return;
}
h->size = 0;
h->capacity = 0;
free(h->heap);
h->heap = NULL;
}
static void checkCapacity(Heap* h){
if (h == NULL){
return;
}
if (h->size >= h->capacity){
h->capacity *= 2;
h->heap = (ElementType*)realloc(h->heap,
sizeof(ElementType) * h->capacity);
}
}
static void swap(int* a, int* b){
assert(a != NULL);
assert(b != NULL);
int temp = *a;
*a = *b;
*b = temp;
}
static void upwardAdjustment(Heap* h, int child){
if (h == NULL){
return;
}
if (child == 0){
return;
}
int parent = (child - 1) / 2;
if (parent < 0 || h->heap[child] <= h->heap[parent]){
return;
}
swap(&h->heap[child], &h->heap[parent]);
upwardAdjustment(h, parent);
}
void heapPush(Heap* h, ElementType x){
if (h == NULL){
return;
}
checkCapacity(h);
++h->size;
h->heap[h->size - 1] = x;
upwardAdjustment(h, h->size - 1);
}
static void downwardAdjustment(ElementType heap[], size_t size, size_t root_index){
if (heap == NULL){
return;
}
size_t left_index = 2 * root_index + 1;
if (2 * root_index + 1 >= size){
return;
}
size_t max_index = left_index;
size_t right_index = 2 * root_index + 2;
if (right_index < size && heap[right_index] > heap[left_index]){
max_index = right_index;
}
if (heap[root_index] < heap[max_index]){
swap(&heap[root_index], &heap[max_index]);
}
else{
return;
}
downwardAdjustment(heap, size, max_index);
}
void heapPop(Heap* h){
if (h == NULL){
return;
}
swap(&h->heap[0], &h->heap[h->size - 1]);
--h->size;
downwardAdjustment(h->heap, h->size, 0);
}
ElementType heapTop(const Heap* h){
assert(h != NULL);
return h->heap[0];
}
size_t heapSize(const Heap* h){
if (h == NULL){
return 0;
}
return h->size;
}
bool heapEmpty(const Heap* h){
if (h == NULL){
return true;
}
return h->size == 0 ? 1 : 0;
}
void heapSort(Heap* h){
if (h == NULL){
return;
}
size_t i = 0;
for (i = 0; i < h->size; ++i){
swap(&h->heap[0], &h->heap[h->size - 1 - i]);
downwardAdjustment(h->heap, h->size - 1 - i, 0);
}
}
void heapDisplay(const Heap* h){
if (h == NULL){
return;
}
size_t i = 0;
printf("The Heap is below: \n");
for (i = 0; i < h->size; ++i){
printf("%d ", h->heap[i]);
}
printf("\n");
}
void heapTest(){
Heap h;
heapInit(&h);
heapPush(&h, 1);
heapPush(&h, 2);
heapPush(&h, 3);
heapPush(&h, 5);
heapPush(&h, 0);
heapPush(&h, 8);
heapPop(&h);
ElementType top = heapTop(&h);
printf("Heap Top: %lu\n", top);
size_t size = heapSize(&h);
printf("Heap Size: %lu\n", size);
if (heapEmpty(&h)){
printf("Heap is Empty!\n");
}
else{
printf("Heap isn't Empty!\n");
}
heapDisplay(&h);
heapSort(&h);
heapDisplay(&h);
}
Java版
public class MyPriorityQueue {
private int[] arr = new int[100];
private int size = 0;
public void offer(int val) {
this.arr[this.size] = val;
++this.size;
shiftUp(this.size - 1);
}
public Integer poll() {
if (this.size == 0) {
return null;
}
int ret = this.arr[0];
swap(0, this.size - 1);
--this.size;
shiftDown(0);
return ret;
}
public Integer peek() {
if (this.size == 0) {
return null;
}
return this.arr[0];
}
private void shiftDown(int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < this.size) {
if (child + 1 < this.size && this.arr[child + 1] > this.arr[child]) {
++child;
}
if (this.arr[parent] < this.arr[child]) {
swap(child, parent);
} else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
private void shiftUp(int index) {
int child = index;
int parent = (child - 1) / 2;
while (child > 0) {
if (this.arr[child] > this.arr[parent]) {
swap(child, parent);
} else {
break;
}
child = parent;
parent = (child - 1) / 2;
}
}
private void swap(int i, int j) {
int temp = this.arr[i];
this.arr[i] = this.arr[j];
this.arr[j] = temp;
}
}
优先级队列
实际中,当我们需要使用堆的时候,我们并不需要去手动实现一个堆,一般的编程语言中都会提供堆这个数据结构。
C++中的优先级队列
首先,我们来看一下官方文档:
看不懂英文不要紧,翻译在下面(O(∩_∩)O哈哈~):
- 优先级队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中的最大者。
- 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先级队列中位于顶部的元素)。
- 优先级队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供了一组特定的成员函数来访问其元素。元素从特定容器的尾部弹出,其称为优先级队列的顶部。
- 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
empty():检查容器是否为空。
size():返回容器中有效元素的数量。
front():返回容器中第一个元素的引用。
push_back():在容器尾部插入元素。
pop_back():删除容器尾部元素。 - 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
- 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap、pop_heap来自动完成此操作。
接口介绍
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:priority_queue默认情况下是大堆。
函数声明 | 接口说明 |
priority_queue(const Compare& x = Compare(), const Container& y = Container()); | 构造一个空的优先级队列 |
priority_queue(InputIterator first, InputIterator last, const Compare& comp = Compare(), const Container& ctnr = Container()); | 用[first, last)区间中的元素构造优先级队列 |
bool empty() const; | 检测优先级队列是否为空,是空返回true,否则返回false |
const value_type& top() const; | 返回优先级队列中最大(最小)元素,即堆顶元素 |
void push(const T& x); | 在优先级队列中插入元素x |
void pop(); | 删除优先级队列中最大(最小)元素,即堆顶元素 |
优先级队列的简单使用
默认情况下,priority_queue是大堆。
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
using namespace std;
int main(){
vector<int> v = {
3, 8, 1, 0, 5, 4, 7, 2, 9, 6
};
// 默认创建为大堆
priority_queue<int> q1;
for (const auto& e : v){
q1.push(e);
}
// 打印堆顶元素
cout << q1.top() << endl;
// 创建小堆
priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
// 打印堆顶元素
cout << q2.top() << endl;
return 0;
}
运行结果如下:
如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供>或者<的重载。
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
using namespace std;
class Date{
public:
Date(int year = 2000, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d) const{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date d) const{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d){
_cout << d._year << "-" << d._month
<< "-" << d._day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
int main(){
// 大堆,需要重载<
priority_queue<Date> q1;
q1.push(Date(2019, 8, 31));
q1.push(Date(2019, 9, 1));
q1.push(Date(2019, 9, 2));
cout << q1.top();
// 小堆,需要重载>
priority_queue<Date, vector<Date>, greater<Date>> q2;
q2.push(Date(2019, 8, 31));
q2.push(Date(2019, 9, 1));
q2.push(Date(2019, 9, 2));
cout << q2.top();
return 0;
}
运行结果如下:
有些情况下,用户可能需要提供比较器规则。
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
using namespace std;
class Date{
public:
Date(int year = 2000, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d) const{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date d) const{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d){
_cout << d._year << "-" << d._month
<< "-" << d._day << endl;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
class Less{
public:
bool operator()(const Date* d1, const Date* d2){
return *d1 < *d2;
}
};
int main(){
priority_queue<Date*, vector<Date*>, Less> q;
q.push(&Date(2019, 9, 1));
q.push(&Date(2019, 8, 31));
q.push(&Date(2019, 9, 2));
// 打印堆顶元素
cout << *q.top();
return 0;
}
运行结果如下:
优先级队列的实际应用
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 将数组中的元素放入优先级队列
priority_queue<int> pq(nums.begin(), nums.end());
// 将优先级队列中前K-1个元素删掉
for(int i = 0; i < k - 1; ++i){
pq.pop();
}
return pq.top();
}
};
Java中的优先级队列
接口介绍
错误处理 | 抛异常 | 返回特殊值 |
入队列 | add(e) | offer(e) |
出队列 | remove() | poll() |
取队首元素 | element() | peek() |
实际应用
我们来看一个问题来理解一下PriorityQueue的使用。
查找和最小的k对数
class Solution {
static class Pair {
public int num1;
public int num2;
public int sum;
public Pair(int num1, int num2) {
this.num1 = num1;
this.num2 = num2;
this.sum = num1 + num2;
}
}
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
List<List<Integer>> result = new ArrayList<>();
if (k < 1) {
return result;
}
PriorityQueue<Pair> queue = new PriorityQueue<>(k, new Comparator<Pair>(){
@Override
public int compare(Pair o1, Pair o2) {
return o2.sum - o1.sum;
}
});
for (int i = 0; i < nums1.length && i < k; ++i) {
for (int j = 0; j < nums2.length && j < k; ++j) {
queue.offer(new Pair(nums1[i], nums2[j]));
if (queue.size() > k) {
queue.poll();
}
}
}
while (!queue.isEmpty()) {
Pair pair = queue.poll();
List<Integer> curPair = new ArrayList<>();
curPair.add(pair.num1);
curPair.add(pair.num2);
result.add(0, curPair);
}
return result;
}
}