堆,是一种树,由它实现的优先级队列的插入和删除的时间复杂度都是O(logN)。
尽管这样删除的时间变慢了一些,但是插入的时间变快得多了。
当速度非常重要,且有很多插入操作时,可以选择 [堆] 来实现优先级队列。
----------------------------------------------------------------
这里的“堆”是一种特殊的二叉树,不要和Java和C++等编程语言里的“堆”混淆。
后者是程序员用new能得到的计算机内存的可用部分。
-----------------------------------------------------------------
堆的介绍:
1.它是完全二叉树。也就是说,除了树的最后一层节点是不需要是满的,其他的每一层从左到右都是满的。如下图所示。
2.它常常用一个数组来实现。
3.堆中的每一个节点都满足堆的条件,也就是说每一个节点的关键字都大于(或等于)这个节点的子节点的关键字。
下图是堆与实现它的数组之间的联系。堆在存储器中的表示是数组,堆只是一个概念上的表示。
树是完全二叉树,并且所有的节点都满足堆的条件。
堆是完全二叉树的事实说明了表示堆的数组中没有“洞”。从下标0---N-1,每一个数据单元都有数据项。
在本篇中,最大的关键字在根节点(root)上,基于这种堆的优先级队列是降序的优先级队列。
-------------------------------------------------------------------------------------------------------------------------------
弱序
堆和二叉排序树相比是弱序的。
因为在二叉排序树中,所有节点的左子节点的关键字都小于右子节点的关键字。
这说明了,在二叉排序树中通过一个简单的算法就可以遍历节点。
但是在堆中,遍历节点是困难的。
对于堆来说,只要求沿着从根到叶子的每一个条路径,节点都是按降序排列的。
指定节点的左边节点或者右边节点,以及上层节点或者下层节点,由于不在同一条路径上,它们的关键字可能比指定节点的关键字或大或小。
除了共享的路径,路径之间都是相互独立的。
除了遍历节点是困难的,由于堆的弱序,删除指定的某一节点也是困难的,因为没有足够的信息来决定选择通过节点的两个子节点中的哪一个走向下一层。
但是可以有序的数组进行遍历进行查找,但是这样的时间复杂度为O(N)。
--------------------------------------------------------------------------------------------------------------------------------------
移除
移除是指,移除根节点。即删除最大(最小)的节点。所以移除是容易的。根在堆数组中的索引总是为0。
但移除了根节点,树就不再是完全的了。数组里就会有一个“洞”,一个空的数据单元,必须要填上。
可以把所有的数组往前移动一位。但是有更高效的办法:
1.移走根
2.把最后一个节点移动到根的位置。
3.一直向下筛选这个节点,直到它在一个大于它的节点之下,小于它的节点之上为止。
最后的节点是树的最右端的节点,移除的过程如下:
在向下进行数据交换的时候,还需要比较两个子节点的大小。
如果是跟两个子节点中较小的进行交换,则这个小的子节点将会成为另外一个大的子节点的父节点,这就违背了堆的条件了。如下图所示:
------------------------------------------------------------------------------------------------------------
插入
插入也是很容易的。
在数组的最后第一个空着的单元插入数据。
但由于插入的数据不知道大小,所以可能会破坏堆的条件。
如果插入的新节点,大于它新得到的父节点,就会造成这种情况,这时候要向上筛选,到达一个比父节点小,但是比自己的子节点大的位置。
如下图所示:
在图中,新插入的节点恰好在根节点上,但是新节点最后也可能落在中间层上。
向上筛选相比较向下筛选简单了许多,因为不用像向下筛选还需要进行子节点的大小比较。
向上筛选的节点只有一个父节点。
比较删除和插入这两个图所示,如果先删除一个节点,再插入相同的一个节点,结果并不一定是回复为原来的堆。
一组给定的节点可以组合成许多合法的堆,关键在于插入的顺序。
------------------------------------------------------------------------------------------------------
交换和复制
上面的插入和删除的两幅图显示了向上筛选和向下筛选的过程中,节点的位置交换的情况。
换位是概念上理解插入和删除最简单的方法。
如下图所示:
在(a)图中,显示了向下筛选过程中使用换位的简单版本。
经过三次换位之后,节点A停在了D位置上,而且B,C,D都会向上移动一层。
但是一次交换需要3次复制,因此在途中则需要9次复制。
所以在(b)图中,使用复制取代交换,可以减少复制的次数。过程如下:
先暂时保存A节点,再将B节点覆盖A节点,C覆盖B,D覆盖C,最后再将A节点复制到之前D节点的位置。
这样复制的次数从9次减少到了->5次。
而且在图中A节点移动了3层,也就是说,随着层数的增加,复制会节省更多的时间。
因为从临时存储区复制,以及复制到临时存储区的位置上,这两次复制的次数在复制的总数中所占的比例更少了。
所以对于很多层的堆,节省的复制次数接近3的倍数。
--------------------------------------------------------------------------------------------
一个小规律:
若数组中节点的索引值为x
1.它的父节点的下标为 (x-1)/2
2.它的左子节点的下标为 2*x + 1
3.它的右子节点的下标为 2*x + 2
------------------------------------------------------------------------------------------
代码:
Node类
package heap;
/**
* Created by Hubbert on 2017/12/15.
*/
public class Node {
private int iData;
public Node( int key ){
this.iData = key;
}
public int getKey(){
return this.iData;
}
public void setKey( int id ){
this.iData = id;
}
}
Heap类
插入,向上筛选:
public class Heap {
private Node [] heapArray;
private int maxSize;
private int currentSize;
public Heap( int max ){
maxSize = max;
currentSize = 0;
heapArray = new Node[maxSize];
}
public boolean isEmpty(){
return currentSize == 0;
}
public boolean insert ( int key ) {
if ( currentSize == maxSize ) {
return false;
}
Node newNode = new Node( key );
heapArray [currentSize] = newNode;
trickUp( currentSize++ );//向上筛选
return true;
}
public void trickUp( int index ){
int parent = ( index - 1 ) / 2;
//保留插入的节点
Node bottom = heapArray[currentSize];
//一直往上筛选,直到该节点比父节点小,比子节点大的位置。
while ( index > 0 && heapArray[parent].getKey() < bottom.getKey() ) {
heapArray[parent] = heapArray[index];
index = parent;
parent = ( parent - 1 ) / 2 ;
}
heapArray[index] = bottom;
}
}
删除根节点,向下筛选:
public void tickDown( int index ){
int largeChile;
Node top = heapArray[currentSize];
while ( index < currentSize/2 ){ //至少需要有一个子节点
int leftChild = index * 2 + 1 ;
int rigthChile = leftChild + 1 ;
//判断是否有右子节点和右子节点是否比左子节点大
//只要不符合其中一个条件,则largechile就为左子节点
if( rigthChile < currentSize
&& heapArray[rigthChile].getKey() > heapArray[leftChild].getKey() ){
largeChile = rigthChile;
} else {
largeChile = leftChild;
}
//如果top节点比子节点大,则跳出循环
if( top.getKey() > heapArray[largeChile].getKey()){
break;
}
heapArray[index] = heapArray[largeChile];
index = largeChile;
}
heapArray[index] = top;
}
改变某一节点的值:
public boolean change ( int index , int newValue ) {
if( index < 0 || index > currentSize ){
return false;
}
int oldValue = heapArray[index].getKey();
heapArray[index].setKey(newValue);
if( oldValue < newValue ){
trickUp(index);
} else {
tickDown(index);
}
return true;
}