二叉树
介绍二叉堆之前首先介绍二叉树。二叉树有一个根节点,节点下又有两个子节点。完全二叉树是指一个二叉树树除了最底层,其他层都是完全平衡的。
完全二叉树最基本的性质就是它的高度是 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(); } }
普林斯顿公开课 算法4-2:二叉堆