前言
在学习了队列和栈之后,我们知道了两种进出方式,队列:先进先出,栈:后进先出,那如果我们想让一堆数据按照其优先级顺序进出呢?所以就引入了一种新的数据结构——二叉堆。
二叉堆
二叉堆是一种基于树的优先级队列,为什么要基于树呢,因为树状结构能使其具有对数级的时间性能。
来张图便于大家理解:
下面我们都以最小堆为例进行讲解。
二叉堆的性质:
结构性:二叉堆是一棵完全二叉树,这保证其有很好的性能,而且可以拿数组来实现,父节点i,子节点2 * i 与2 * i + 1。
有序性:二叉堆是关于元素的优先级有序的,即父节点的优先级总是高于它的两个子节点的优先级,堆的根节点的优先级总是所有 元素中最大的。
基本实现
存储实现:
二叉堆是一棵完全二叉树,所以可以使用数组来进行存储。在记录时记下当前数组中的元素数目,和数组的最大规模。
class priorityQueue{
private:
int *arr;
int curSize;
int maxSize;
}
enQueue操作:
比如说我们想在上面树中插入x = 15,我们就在树的最末端插入一个空穴,然后我们发现这样破坏了二叉堆的有序性,所以我们就让空穴上冒,让x与空穴的父节点比较,x < 31,那样就交换空穴与其父节点,继续比较,x < 21,继续交换,然后发现x > 13,这时我们将x放入空穴,这时二叉堆又恢复了有序性。
这里还要注意在插入前要判断数组会不会炸,如果会炸要doubleSpace。
void doubleSpace(){
int i;
int *tmp = arr;
arr = new int[maxSize * 2];
maxSize = maxSize * 2;
for(i = 1; i < maxSize; i++){
arr[i] = tmp[i];
}
delete []tmp;
}
void enQueue(int x){
if(curSize == maxSize - 1){
doubleSpace();
}
curSize++;
arr[curSize] = x;
//push up
int hole = curSize;
while(true){
if(hole == 1 || arr[hole / 2] <= x){
arr[hole] = x;
break;
}
arr[hole] = arr[hole / 2];
hole = hole / 2;
}
}
deQueue操作:
二叉堆的deQueue操作就是推出优先级最高的元素,那不就根节点空了吗,怎样恢复二叉堆的有序性呢?
如图,我们先推出了13,然后将堆最末端的元素32补到根节点,然后删去末端元素,之后对根节点进行下推操作。我们找到根节点的左右子节点中的最小值16,32大于16,然后就16上冒到根节点的位置,16的位置变为空穴,之后32再与空穴的左右子节点中的最小值进行比较,发现32大于19,那么19上冒到空穴的位置,19的位置变为空穴。然后发现空穴再没有子节点了,那就将32放入空穴,大功告成,二叉堆有恢复了有序。
void pDown(int id){
int x = arr[id];
int hole = id;
int child = hole * 2;
if(arr[child] > arr[child + 1]){
child++;
}
while(true){
if(hole * 2 > curSize || x < arr[child]){
arr[hole] = x;
break;
}
arr[hole] = arr[child];
hole = child;
child = hole * 2;
if(child != curSize && arr[child] > arr[child + 1]){
child++;
}
}
}
int deQueue(){
int minn = arr[1];
arr[1] = arr[curSize];
curSize--;
pDown(1);
return minn;
}
buildTree操作:
我们学习了enQueue操作,为什么还要学建树呢,直接不断的enQueue不就行了。行是行,但这样enQueue一个元素是O(logn),总时间复杂度达到了O(nlogn),是不是有种不祥的预感(这怕是要超时啊),那怎么优化呢?
我们可以直接将数据建成一棵二叉树,当然这时的二叉树乱的一匹,我们想恢复其有序性,只要从树的前一半节点进行下推操作即可,这样的时间复杂度为O(n),其实这里我也很懵逼,怎么就O(n)了,此处转载知乎大神的讲解,真的niuB:
void buildHeap(){
int i;
curSize = N;
for(i = 1; i <= curSize; i++){
arr[i] = item[i - 1];
}
for(i = curSize / 2; i >= 1; i--){
pDown(i);
}
}
完整代码:
#include <iostream>
using namespace std;
const int N = 1000;
int item[N];
class priorityQueue{
private:
int *arr;
int curSize;
int maxSize;
public:
priorityQueue(int size = 1000){
arr = new int[size];
maxSize = size;
curSize = 0;
}
~priorityQueue(){
delete []arr;
}
void doubleSpace(){
int i;
int *tmp = arr;
arr = new int[maxSize * 2];
maxSize = maxSize * 2;
for(i = 1; i < maxSize; i++){
arr[i] = tmp[i];
}
delete []tmp;
}
void enQueue(int x){
if(curSize == maxSize - 1){
doubleSpace();
}
curSize++;
arr[curSize] = x;
//push up
int hole = curSize;
while(true){
if(hole == 1 || arr[hole / 2] <= x){
arr[hole] = x;
break;
}
arr[hole] = arr[hole / 2];
hole = hole / 2;
}
}
int getHead(){
return arr[1];
}
void pDown(int id){
int x = arr[id];
int hole = id;
int child = hole * 2;
if(arr[child] > arr[child + 1]){
child++;
}
while(true){
if(hole * 2 > curSize || x < arr[child]){
arr[hole] = x;
break;
}
arr[hole] = arr[child];
hole = child;
child = hole * 2;
if(child != curSize && arr[child] > arr[child + 1]){
child++;
}
}
}
int deQueue(){
int minn = arr[1];
arr[1] = arr[curSize];
curSize--;
pDown(1);
return minn;
}
void buildHeap(){
int i;
curSize = N;
for(i = 1; i <= curSize; i++){
arr[i] = item[i - 1];
}
for(i = curSize / 2; i >= 1; i--){
pDown(i);
}
}
void printHeap(){
int i;
for(i = 1; i <= curSize; i++){
cout << arr[i] << " ";
}
cout << endl;
}
};
总结
二叉堆是一种常用于优先级队列的数据结构,当你想要快速的得到优先级最高的元素时,二叉堆可以很好的满足这一需求,因为二叉堆提取优先级最高的元素是O(1)的,在其他方面,二叉堆也有着对数级别的复杂度,性能很好。主要应用有A*寻路、Dijkstra's算法(计算最短路径)等。