数据结构中的堆是一种特殊的二叉树(不要和Java、c++中的“堆”混淆,后者指的是程序猿用new能得到的计算机内存的可用部分)。
堆是具有如下特点的二叉树:
a)它是完全二叉树。也就是说除了树的最后一层节点不需要是满的,其他的每一层从左到右都完全是满的。
b)它常常用一个数组实现。树的实现可以使用数组而不是由引用连接起来的各个节点来存储二叉树。
c)堆中的每个节点都满足堆的条件,也就是说每一个节点的关键字都大于(或等于)这个节点的子节点的关键字。
如下图所示,堆与实现它的数组的关系:
堆在存储器中的表示是数组,堆只是一个概念上的表示。注意树是完全的二叉树,并且所有的节点都满足堆的条件。堆是完全二叉树的事实表明了堆在数组中没有“洞”。从0到N-1,每一个数据单元都有数据。基于这种思想,设节点的索引值是index,则节点的左子节点是:2*index+1,它的右子节点:2*index+2,它的父节点是(index-1)/2。堆主要用于实现优先级队列(因为我们可以在常数时间内获得堆中(优先级)最大或者最小的元素,这对于优先级队列来说是很重要的,而且堆的插入相率相对数组是高的,每次插入新元素都会自动的排序,保证最大或最小的元素位于根节点)。优先级队列和实现它的堆之间有着非常紧密的关系。
堆和二叉树相比是弱序的,在二叉搜索树中所有节点的左子孙的关键字都小于右子孙的关键字。因此,按序遍历一个二叉搜索树是很容易的,但是在堆中,按序遍历节点是困难的,这是因为堆的组织规则比二叉搜索树的组织规则弱。对于堆来说,只要求沿着从根到叶子的每一条路径,节点都是降序的。指定节点的左边节点和右边节点,以及上层节点或者下层节点由于不在同一条路径上,它们的关键字可能比指定节点的关键字或大或小。除了有共享节点的路径,路径之间是相互独立的。
由于堆是弱序的,所以一些操作是困难或者不可能的。除了不支持遍历之外,也不能在堆上遍历地查找指定关键字,因为在查找的过程中,没有足够的信息来决定选择通过节点的两个子节点中的哪一个走向下一层。
堆的这种组织似乎非常接近无序。不过对于快速移除最大节点的操作以及快速插入新的节点的操作,这种顺序已经足够了。这些操作是使用堆作为优先级队列时所需的全部操作。
在讲述堆的插入和删除之前,先讲一个换位的概念
a在进行三次交换,却需要九次复制,而b中三次换位值进行五次复制。因此,在堆的插入删除中,我们都采用b中方式进行换位。
1、插入
插入相对删除更容易一些,插入是一个向上筛选的过程,因为只有一个父节点,因此 只需和父节点比较&交换即可。如下图:
沿着父节点,一直向上筛选,直到找到合适的位置。实现如下:
public void insert(Node node) {
if (currentSize == maxSize) {
return;
}
data[currentSize] = node;
trickleUp(currentSize++);
}
// 因为父节点只有一个,所以可以沿着父节点查找合适的位置
private void trickleUp(int index) {
int parent = (index - 1) / 2;
Node bottom = data[index];
//index:待插入位置索引;parent:需要进行比较的父节点索引;
while (index > 0 && data[parent].getData() < bottom.getData()) {
data[index] = data[parent];
index = parent;
parent = (index - 1) / 2;
}
data[index] = bottom;
}
2、删除
思想:
a)、移走根;
b)、把最后一个节点移动到根的位置;
c)、一直向下筛选这个节点,直到它在一个大于它的节点之下,小于它的节点之上;
最后一个节点是树最底层的最右端的数据项,它对应数组中最后一个填入的数据单元;以上过程实际上是一个向下筛选的过程,不过每次都要比较两个子节点的大小,然后和较大者进行交换;
如下图所示:
其实现如下:
public void remove() {
if (currentSize == 0) {
return;
}
data[0] = data[--currentSize];
trickleDown(0);
}
private void trickleDown(int index) {
Node top = data[index];
int largeChild = 0;
while (index < currentSize / 2) {
int leftChild = 2 * index + 1;
int rightChild = 2 * index + 2;
if (rightChild < currentSize
&& data[leftChild].getData() > data[rightChild].getData()) {
largeChild = leftChild;
} else
largeChild = rightChild;
if(top.getData()>data[largeChild].getData()){//已经找到合适的位置
break;
}
data[index] = data[largeChild];
index = largeChild;
}
data[index] = top;
}
堆也可以通过真正的树实现,这里不再讨论。
堆排序:堆数据结构的效率使它引出一种出奇简单,但却很有效率的排序算法,称为堆排序。堆排序的基本思想是使用普通的堆的insert()在堆中插入全部无序的数据项,然后重复remove,就可以按序移除所有数据项了。
for(j=0;j<size;j++){
heap.insert(anArray[j]);}
for(j=0;j<size;j++){
heap.remove(anArray[j]);}
堆排序要比快速排序慢一些,部分原因是trickleDown里while循环的操作要比快速排序里循环中的操作要多。但是有一些技巧可以使堆排序更有效,这里不再叙述。