首先,要说明一下本篇文章说的堆和JVM内存结构里边的堆是两个东西。它是优先级队列的底层数据结构。
- new出来的空间(new一个对象)-------->这里的堆指的是一块具有特殊作用的内存空间(JVM内存区域中)
- 优先级队列的底层数据结构 ------->这里的堆是一种数据结构
那么什么是堆这种数据结构呢?
堆的基本概念
这个堆是建立在完全二叉树的存储方式上,我们知道完全二叉树适合用顺序的存储方式来存储,因为它是没有满的满二叉树,所以通过数组来顺序存储效率是比较高的。
堆的概念:
堆就是一棵完全二叉树,采用顺序的方式(数组)来高效存储。
堆的分类: 堆又分为大根堆(根节点的值比左右孩子都大)和小根堆。如上图所示,就是一个小根堆。
堆的性质:
将元素存储到数组中后,可以根据二叉树的性质对树进行还原。假设i为节点在数组中的下标,则有:
- 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
- 如果2 * i + 1小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
- 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
堆的常用方法
1、构建一个堆
我们这里以构建一个小堆为例,小堆的特点就是这棵二叉树每个根节点的值都小于左右孩子的值。
我们知道,堆其实就是一个按顺序存储的二叉树,只要给定了数组,我们就可以按照数组的下标将这棵树构建出来,比如给定的数组为:[27,15,19,18,28,34,65,49,25,37 ],那么我们就可以构建出如下图的二叉树:
我们会发现,按照数组下标创建出来的堆,并不满足大根堆或者小根堆,对于根节点的左右子树都是满足根节点小于左右节点的值,只有根节点大于左右子树,那么此时只需要将根节点向下调整好即可。
向下调整
我们这里以调整为小根堆为例,当根节点的左右子树都满足小堆的性值时,即 使用一次向下调整就可以满足堆的性值。那我们来看看如何实现这个向下调整函数:
思路:如果根节点比左右孩子都大:找到较小的孩子将其与根节点进行交换。依次向下循环,直到叶子节点
代码:
public void shiftDown(int[] array,int parent)//parent是下标
{
int child=parent*2+1;//孩子的下标
while(child<array.length)//此时只能保证有左孩子
{
//找到左右孩子较小的(右孩子存在的前提下)
if(child+1<array.length && array[child+1]<array[child])
{
child=child+1;
}
//开始向下调整
//检测双亲是否比较小的孩子大
if(array[parent]>array[child])
{
//交换
int tmp=array[parent];
array[parent]=array[child];
array[child]=tmp;
//向下更新孩子和双亲的下标
parent=child;
child=parent*2+1;
}else{
return;
}
}
}
注意: 在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆了才可以向下调整。
时间复杂度分析: 最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(log2(n))
堆的创建
堆的创建其实也就是将完全二叉树调整为堆(以小堆为例),我们先分两种情况:
- 如果根节点的左右子树都已经时小堆了,那么
只需要一次向下调整
.就可以成为小堆 - 如果根节点的左右子树都不满足堆的性质,那麽我们就需要从左右子树的
最下面的二叉树开始调整,依次向上,直到根节点
。所以关键就是需要找到倒数第一个非叶子节点(也就是调整的第一个节点)- 倒数第一个非叶子节点就是最后一个节点的双亲,最后一个节点的下标是:arr.length-1;所以倒数第一个非叶子节点的下标就是:( (arr.length-1) -1)/2===>(arr.length-2)/2
代码:
public void createHeap(int[] array) {
//从倒数第一个非叶子节点开始调整
int adjust=(arr.length-2)/2;
for(i=adjust;i>=0;i--)
{
shiftDown(array,adjust);
}
}
时间复杂度分析: 最坏情况:O(n*log(2n));最优(只需要一次向下调整):O(log(2n))
2、插入元素
每次插入的时候是向数组中插入元素,所以每次都是插在数组尾部。所以需要向上一层一层调整将整个完全二叉树.
步骤:
- 先检测是否需要扩容
- 先将元素放入到底层空间中:尾插
- 将最后新插入的节点向上调整,直到满足堆的性质
public boolean offer(int x)
{
//1.先检测是否需要扩容,我们这里以简单的2倍进行扩容
if(size==array.length)
{
array,length=2*array.length;
}
//2.将元素尾插到数组中
size+=1;
array[size]=x;
//3.向上调整
shiftUp(size-1);
return true;
}
//向上调整
private void shiftUp(int child)
{
int parent=(child-1)/2;
while(child!=0)
{
if(array[parent]>array[child])
{
//交换
int tmp=array[parent];
array[parent]=array[child];
array[child]=tmp;
//更新下标
child=parent;
parent=(child-1)/2;
}
}
}
3、删除元素
注意:堆的删除一定删除的是堆顶元素。 所以步骤如下:
- 判断堆是否为空
- 空:return
- 不为空:将最后一个元素和堆顶元素进行交换,并删除堆中最后一个元素(也就是将之前的堆顶元素删除)
- 调整0号下标这棵树(一次向下调整即可)
代码:
public void pop()
{
//判断堆是否为空
if(array.length==0)
{
return;
}
int tmp=array[0];
array[0]=array[size-1];
array[size-1]=tmp;
//删除
array.length-=1;
//一次向下调整
shiftDown(array,0);
}
堆的应用
堆这种数据结构应用主要有两个方面:
- 用堆作为底层结构封装优先级队列:将在后续的优先级队列的包括中具体写出
- 堆排序:具体见排序的博客:链接: 八大排序(1).