堆,可分为大顶堆和小顶堆,以大顶堆为例,其特点为父节点要比起子节点都大。
所以堆支持在最快的速度获得最大值(对于大顶堆)/ 最小值(对于小顶堆)
同时,堆的插入、删除的效率都很不错 O(logn)
堆的形状是一个完全二叉树,我们用数组来存储表示堆
所以关于对就有以下的特性:
- 对于编号为p的节点,其父节点、左子节点、右子节点的编号分别为:(p-1) / 2,2p+1,2p+2
- 对于节点数量为n的堆,任意节点的编号p都小于n,且当 n/2-1 < p <= n时,节点为叶子节点
- 堆的插入:已知一个堆,将一个给定的值Val插入堆中
首先直接把Val插入堆结尾,即编号最大的位置,因为这可能会破坏堆的特性,所以要从它开始往上进行调整位置
bool insert(int val)
{
if(n==size) //堆已满,无法插入
return false;
pos = n++;
Heap[pos] = val;
while(pos && Heap[pos]>Heap[parent(pos)])
{
swap(pos,parent(pos));
pos = parent(pos);
}
return true;
}
- 堆的创建:用一个给定大小的数组来创建一个堆
第一种方法:遍历给定的数组,一个一个插入堆中,每次插入的过程都保持堆的特性,这样通过n次插入操作就可以构成一个堆,复杂度为O(nlogn)
第二种方法:直接对一个数组进行调整,自底向上。原理是:对于一个二叉树,左子树和右子树都是堆,则可以通过适当的位置调整,保持堆的特性
void siftdown(int pos)
{
while(!isLeaf(pos)){ //判断是否是叶结点,如果是则不操作
int j = leftchild(pos); //左孩子
int rc = rightchild(pos); //右孩子
if((rc<n)&&Heap[j]<Heap[rc])
j = rc; //将较大孩子的编号赋值给j
if(Heap[pos]>=Heap[j]) return; //如果父节点大于较大孩子,则可直接返回,无需调整
swap(pos,j); //否则交换父节点和较大孩子的值
pos = j; //并将pos指向交换后的位置
}
}
void buildHeap(int arr[],int n)
{
//对于所有非叶子节点进行siftdown操作,即调整以它为根的二叉子树,使之成为堆
for(int i = n/2 - 1; i>=0 ;i--)
{
siftdown(i);
}
}
- 堆的删除:针对于某个位置,进行删除,保证完全二叉树以及堆的特性
void delete(int pos)
{
swap(pos,--n);<span style="white-space:pre"> </span>//将堆尾元素的值与要删除的元素的值进行swap
while(pos&&Heap[pos]>Heap[parent(pos)])<span style="white-space:pre"> </span>//如果当前元素的值大于其父亲的值,说明不符合堆特性,要调整
{
swap(pos,parent(pos));<span style="white-space:pre"> </span>//交换当前元素与父亲的值
pos = parent(pos);<span style="white-space:pre"> </span>//以父亲节点的位置作为当前位置
}
siftdown(pos);<span style="white-space:pre"> </span>//向上调整好之后就满足了子树都为堆的特性,可用siftdown将整棵树都变成堆
}
- 堆的应用:
说到底,应该怎么应用堆这个数据结构呢?这里我收集了两个简单的案例,都很简单的,但是告诉我们其实不仅是堆,任何数据结构都可以被我们应用起来,关键看怎么思考使用它们
第一个:如果我们需要一个数据结构能够支持快速插入、删除、获得最大值操作,那堆就是非常高效地,比如我们可以用堆来实现一个多任务操作系统中的任务优先级队列,来调度作业,每次都会取出优先级最高的作业来执行,因为堆的插入、删除和获取最大值的效率很高,所以会非常高效
第二个:用堆我们可以实现堆排序