注意:这里的堆和 Java,C++ 等编程语言在内存中的“堆”是不一样的,这里的堆是一种树结构。
一、堆的定义
(1)它是完全二叉树
完全二叉树——对于一棵具有 n 个节点的二叉树按层序编号,如果编号为 i 的节点与同样深度的满二叉树中编号为 i 的节点在二叉树中的位置完全相同,则这棵树称为完全二叉树。
(2)通常用数组来实现
用数组实现二叉树。假设某个节点的索引为 index,则
- 该节点的左子节点的索引是 ;
- 该节点的右子节点的索引是 ;
- 该节点的父节点的索引是 。
(3)堆中每个节点的关键字都大于其子节点的关键字
注意堆与二叉搜索树的区别:
- 对于二叉搜索树,每个节点的关键字都大于其子节点的关键字,并且,每个节点的左子节点的关键字都小于右子节点的关键字,通过中序遍历就可实现升序遍历;
- 对于堆,只要求每个节点的关键字都大于其子节点的关键字,而每个节点的左、右子节点没有确定的大小关系。只有沿着从根节点到叶节点的每一条路径上是降序排列的。因此,相比于二叉搜索树,堆是弱序的。
二、堆的主要操作
优先级队列提供了删除最大(或最小)数据项和插入数据项的方法,可以用有序数组来实现。这种实现方式删除操作的时间复杂度为 O(1),但是插入操作的时间复杂度还是O(N),因为每次插入平均需要移动一半的数据项,来保证插入后的数组依然有序。
用堆实现优先级队列时,其插入和删除的时间复杂度都为 O(logN),删除的时间变慢,但是提高了插入的时间。所以当有很多插入操作时,可以选择用堆来实现优先级队列。
1、遍历和查找
由于堆是弱序的,所以在遍历和查找的过程中,没有足够的信息来确定选择两个子节点的哪一个来走向下一层,因此,堆是不支持查找和遍历的。堆的主要作用就是快速删除最大(最小)节点和快速插入数据。
2、删除
堆的删除操作是指删除关键字最大的节点,即根节点。根节点移除后,还需将这个空的节点填上。
两种方法:
(1)所有数据项向前移动一个单元,但很费时;
(2)把最后一个节点放到根节点的位置,再向下筛选,使得该节点位于一个大于它的节点之下,小于它的节点之上(向下筛选时,将目标节点和其子节点比较,哪个子节点大就和哪个交换位置)。过程图示如下:
3、插入
插入节点时,先将节点插入数组最后一个数据的后一个位置,然后向上筛选,将该节点放在合适的位置(向上筛选,和其父节点比较,放到比其父节点小的位置即可)。
三、代码实现
class Node{
private int data;
public Node(int data){ this.data = data; }
public int getData() { return data; }
public void setData(int data) { this.data = data; }
}
public class Heap {
private Node[] heapArray;
private int maxSize;// 数组的大小
private int num;// 数组中数据项的个数
public Heap(int size){
maxSize = size;
heapArray = new Node[maxSize];
num = 0;
}
public boolean isEmpty(){
return num == 0;
}
public boolean isFull(){
return num == maxSize;
}
// 移除:移除关键字最大的节点,即根节点
public Node remove(){
Node root = heapArray[0];
heapArray[0] = heapArray[--num];
downward(0);
return root;
}
// 向下调整:参数index为要调整节点的索引
public void downward(int index){
Node top = heapArray[index];
int indexOfBigChild;// 较大孩子的索引
while(index < num / 2){
int iLeft = 2 * index + 1;// 左孩子的索引
int iRight = 2 * index + 2;// 右孩子的索引
if(iRight < num && // 右孩子是否存在
heapArray[iLeft].getData() < heapArray[iRight].getData()){
indexOfBigChild = iRight;
}else{
indexOfBigChild = iLeft;
}
// 目标节点的数据大于子节点的数据时,调整完成,跳出循环
if(top.getData() > heapArray[indexOfBigChild].getData()){
break;
}
// 较大的子节点向上移
heapArray[index] = heapArray[indexOfBigChild];
index = indexOfBigChild;
}
heapArray[index] = top;
}
// 插入
public void insert(int data){
if(isFull()){
heapArray = Arrays.copyOf(heapArray, 2 * maxSize); ;
}
Node node = new Node(data);
heapArray[num] = node;
upward(num);
num++;
}
// 向上调整:参数index为要调整节点的索引
public void upward(int index){
int indexOfParent = (index - 1) / 2;
Node bottom = heapArray[index];
while(index > 0 &&
// 若新节点的数据比其父节点的数据大,则需要调整
bottom.getData() > heapArray[indexOfParent].getData()){
heapArray[index] = heapArray[indexOfParent];// 父节点向下调整
index = indexOfParent;
indexOfParent = (index - 1) / 2;
}
heapArray[index] = bottom;
}
// 打印:用数组存储,打印的顺序就是层序遍历
public void display(){
if(isEmpty()){
System.out.println("Heap is empty!");
return;
}else{
for (int i = 0; i < num; i++) {
System.out.print(heapArray[i].getData() + " ");
}
System.out.println();
}
}
public static void main(String[] args) {
Heap heap = new Heap(12);
heap.insert(99);
heap.insert(80);
heap.insert(57);
heap.insert(60);
heap.insert(70);
heap.insert(33);
heap.insert(6);
heap.insert(40);
heap.insert(20);
heap.insert(60);
heap.insert(30);
heap.insert(31);
heap.display();
heap.remove();
heap.display();
}
}
测试程序中的数据就是删除算法中图示的数据,测试结果如下:
99 80 57 60 70 33 6 40 20 60 30 31
80 70 57 60 60 33 6 40 20 31 30