堆
堆是一颗特殊的二叉树,什么样的二叉树才是堆?只需要满足一下两个条件,那么它就是堆。
- 堆是一颗完全二叉树
- 堆中每一个节点的值都必须大于等于(或者小于或等于)其子树中每一个节点的值。
对于每个节点的值大于等于子树中每个节点中的值的堆,称之为大顶堆。对于每个节点的值小于或等于子树中每个节点的值的堆,称之为小顶堆。
如下图:
编号1和编号2是大顶堆,编号3是小顶堆,编号4不是堆。
如何实现一个堆?
要实现一个堆,需要先知道,如何存储一个堆以及堆支持哪些操作。
堆中数据的存储
堆是一颗完全二叉树,所以堆一般都是使用数组来存储数据的。把堆中的元素从上到下,从左至右,从数组的第一个位置开始依次存储在数组中。如下图。如果一个节点在数组中的下标为i。可以计算该节点的父节点的下标为i/2。其左子节点的下标为2*i,右子节点的下标为2*i+1。
堆化
如果往堆中插入一个数据元素,需要判断其是否满足堆的特性,如果不合符,我们需要对其进行调整,让其重新满足堆的特性。而这个过程称之为堆化。
堆化的过程有两种,一种是从下往上堆化,一种是从上往下堆化。
后面会对这两个过程进行介绍。
堆所支持的操作
这里会以大顶堆为例,介绍插入一个数据元素和删除堆顶元素的操作。
插入一个数据元素
往堆中插入一个数据元素如下图。
ru
会把数据元素添加到数组的末尾,需要判断这个插入的数据元素是不是满足堆的特性,如果不满足,则需要与父节点交换位置。直至满足堆的特性为止。这样的一个过程是一个从下往上堆化的过程。如下图
代码实现:
public class Heap {
private int[] heap; //数组,从下标1开始存储数据
private int n; //堆中可存储的最大数据个数
private int count; //堆中已经存在的数据个数
public Heap(int capacity) {
heap =new int[capacity+1];
this.n = capacity;
count=0;
}
//插入
public boolean insert(int num){
if(count>=n){
System.out.println("堆满");
return false;
}
++count;
heap[count]=num; //把插入的数据元素放入堆的末尾
int i=count;
//从下往上堆化的过程
while (i/2>0 && heap[i]>heap[i/2]){
swap(heap,i,i/2);
i=i/2;
}
return true;
}
private void swap(int[] array,int x,int y){
int temp=array[x];
array[x]=array[y];
array[y]=temp;
}
}
删除堆顶元素
删除堆顶元素,然后把最后一个元素放到堆顶,判断该元素是否满足堆的特性,也就是与子节点进行比对,如果不满足,则与子节点交换位置,直至满足堆的特性为止。这样堆化数据元素的过程是一个从上往下堆化的过程。
如下图:
代码实现。
public boolean deleteMax(){
if(count==0) return false;
heap[1]=heap[count];
--count;
downHeapify(heap,count,1);
return true;
}
//对数组中下标为i的数据元素进行从上往下堆化
//n为数组中数据元素的个数,i为要堆化的数据元素的索引
private void downHeapify(int[] array,int n,int i){
while (true){
int pos=i;
if(2*i<=n && array[2*i]>array[i]) pos=2*i; //与左子节点进行比较
if(2*i+1<=n && array[2*i+1] >array[pos]) pos=2*i+1;
if(pos == i)break;
swap(array,i,pos);
i=pos;
}
}
堆排序
堆排序大致可以分为两个步骤:建堆和排序。
建堆
首先将数组原地建成一个堆,就是在原数组上进行操作,建堆过程有两种思路。
第一种实现思路,就是上面讲的插入一个元素的思路,这种思路跟插入排序的思路有点类似。把数组分为已堆化部分和未堆化部分,把数组中未堆化部分逐一插入已堆化部分的尾部,在从下到上堆化。
第二种思路,把数组中的数据元素构建成完全二叉树的数据结构,把所有非叶子节点从上往下堆化。因为堆中的数据元素是从数组的第一个数据元素开始存储的。所以最有一个非叶子节点的下标为n/2(n为堆中元素个数)。也就是把下标为为n/2到下标为1的数据元素依次从上往下堆化。如下图:
需要依次对8、19、5、7这四个数据元素进行从上往下堆化。堆化后如下图所示
代码实现:
public class HeapSort {
public void buildHeap(int[] array,int n){ //n为堆中数据元素的个数
for (int i=n/2;i>=1;i--){
downHeapify(array,n,i);
}
}
//对数组中下标为i的数据元素进行从上往下堆化
//n为数组中数据元素的个数,i为要堆化的数据元素的索引
private void downHeapify(int[] array,int n,int i){
while (true){
int pos=i;
if(2*i<=n && array[2*i]>array[i]) pos=2*i; //与左子节点进行比较
if(2*i+1<=n && array[2*i+1] >array[pos]) pos=2*i+1;
if(pos == i)break;
swap(array,i,pos);
i=pos;
}
}
private void swap(int[] heap, int a, int b) {
int t = heap[a];
heap[a] = heap[b];
heap[b] = t;
}
}
测试代码:
@Test
public void test(){
int[] array={0,7,5,19,8,4,1,20,13,16};
buildHeap(array,array.length-1);
System.out.println(Arrays.toString(array));
}
测试结果:
建堆的时间复杂度分析:
叶子节点不需要堆化,所以需要从倒数第二层开始,每个节点堆化的过程中,需要比较和交换的节点个数跟高度成正比。
每一层的节点个数和对应的高度,如下图所示
可推导出公式如下
进一步推导:把公式左右都乘以2,就得到另一个公式。我们将错位对齐,然后使用减去。如下图所示:
推导如公式如下图:
其中高度h=。可以得到时间复杂度为O(n);
排序
排序的算法跟移除堆顶元素的算法差不多。排序算法是依次把堆顶元素和数组的最后一个数据元素交换位置,然后排除最后一个数据元素,对堆顶元素向下堆化。如下图:
代码实现:
public void sort(int[] array,int n){ //n为堆中数据元素的个数
buildHeap(array,n);
while (n>1){
swap(array,n,1); //交换堆顶元素与与未排序部分最有一个数据元素的位置
--n;
downHeapify(array,n,1); //堆顶元素向下堆化
}
}
排序算法的时间复杂度为O(),而建堆的时间复杂度为O(n)。根据时间复杂度的加法法则,可得到堆排序的时间复杂度为
O()
总结:知识点
堆的基本概念
如何创建一个堆?
使用数组存储堆。
根据节点的下标,获取该节点父节点下标和该节点左右子节点下标
堆化:从上往下堆化和从下往上堆化
堆的新增和删除堆顶元素
堆排序:建堆与排序。以及复杂度分析
参考:数据结构与算法之美--王争