二叉树
介绍二叉堆之前首先介绍二叉树。二叉树有一个根节点,节点下又有两个子节点。完全二叉树是指一个二叉树树除了最底层,其他层都是完全平衡的。
完全二叉树最基本的性质就是它的高度是 floor(lgN)。
二叉堆
二叉堆是完全二叉树的一种,每个节点对应一个数值,而且这个数值都大于等于它子节点的数值。
下图是一个二叉堆。
二叉堆的储存
由于二叉堆是完全二叉树,所以它可以用一个数组进行储存。所以不需要创建节点对象,再建立节点之间的连接。这样节省了很多开销。
用数组a[]表示一个二叉堆有以下特性:
-
a[0]不表示任何数据,这样后续的计算就简化了很多
-
a[1]是根节点
-
a[2]a[3]是二叉树第一层的两个节点
-
a[4]a[5]a[6]a[7]是二叉树的第二层节点
以此类推
下图展示了堆的数据结构用数组进行表示的示意图。
二叉堆的访问
二叉堆有以下访问规则:
-
二叉堆的根就是a[1],也是最大的一个元素。
-
在a[k]位置的节点,其父节点为k/2
-
在a[k]位置的节点,其子节点分别为2k和2k+1
二叉树的操作
二叉堆中每个子节点都不能比它的父节点大,然而在修改二叉树之后往往会违反二叉树的性质。
Swim操作
当二叉树的子节点比父节点大时,怎么办呢?没关系,只要按照以下步骤进行就行了:
-
将违反规则的节点与父节点进行交换
-
重复第一步,直到所有的节点都二叉堆的性质为止
该操作称为swim操作
Sink操作
当二叉树的父节点比任意一个子节点小时,怎么办呢?没关系,只要按照以下步骤进行就行了:
-
将违反规则的子节点与两个子节点中较大的子节点进行交换
-
重复第一步,直到符合二叉堆的性质为止
该操作称为sink操作
删除操作
从堆中删除最大的元素,需要执行以下步骤:
-
将最小的元素与根节点进行交换
-
对根节点执行sink操作,将最小的节点沉淀到二叉堆的最底部
这样,删除操作最多需要2lgN次比较
复杂度
二元堆的插入操作复杂度是logN,删除操作的复杂度是logN。
d元堆的插入操作复杂度是log_d(N),删除操作的复杂度是d log_d(N)。
斐波那契堆的插入操作复杂度是1,删除操作平均复杂度是logN。
不变性
对于二叉堆,需要保元素自身不会发生变化,这样才能保证二叉堆的算法能够正常工作。
因此,在Java中,二叉堆的数据类型尽量选择不可变的数据类型。所谓不可变的类型就是指当对象创建完毕之后,对象中的内容就无法再改变了。
java中不可变的类型有String Integer Double Color Vector Transaction Point2D。
可变的类型有StringBuilder Stack Counter Java数组。
不可变类型的好处就是能确保优先级队列能够正常工作,坏处就是每次有新值的时候都要创建一个新的对象,造成额外的开销。
代码
// 用二叉堆实现的最大优先级队列
public class MaxHeapPQ<Item extends Comparable<Item>> {
private Item[] items;
private int N;
public MaxHeapPQ(int capacity) {
items = (Item[])new Comparable[capacity+1]; //capacity+1是因为a[0]不储存任何数据,所以需要额外的一个元素。
}
public boolean isEmpty() {
return N==0;
}
public void insert(Item item) {
N++;
items[N] = item;
swim(N);
}
public Item popMax() {
Item result = items[1];
SortUtil.exch(items, N, 1);
items[N] = null; // 防止产生游离对象造成内存泄露
N--;
sink(1);
return result;
}
public int size() {
return N;
}
private void swim(int k) {
// 一直循环直到到达根节点
while(k > 1) {
Item child = items[k];
Item parent = items[k/2];
// 如果父节点比子节点还小,就将父子进行交换
if(SortUtil.less(parent, child)) {
SortUtil.exch(items, k, k/2);
k /= 2;
} else {
break;
}
}
}
private void sink(int k) {
// 一直循环直到没有子节点
while(k*2 <= N) {
int parent = k;
int child1 = k*2;
int child2 = k*2+1;
// 如果父节点比任意一个子节点小
if(less(parent, child1) || less(parent, child2)) { // 注意:这里不能用SortUtil.less进行比较,因为空值也要进行比较
// 选出较大的子节点
int greater;
if(less(child1, child2)) { // 注意:这里child1和child2位置不要写反了
greater = child2;
} else {
greater = child1;
}
// 将较大的子节点与父节点交换
SortUtil.exch(items, greater, parent);
// 准备下一次循环
k = greater;
} else {
break;
}
}
}
// 判断是否小于。将null值看成无穷小
private boolean less(int a, int b) {
if(a > N) return true;
if(b > N) return false;
return SortUtil.less(items[a], items[b]);
}
private void debugPrint() {
for(int i=0;i<N;i++){
System.out.print(items[i+1]);
System.out.print(" ");
}
System.out.println();
}
}