首页 > 代码库 > 普林斯顿公开课 算法4-2:二叉堆

普林斯顿公开课 算法4-2:二叉堆

二叉树


介绍二叉堆之前首先介绍二叉树。二叉树有一个根节点,节点下又有两个子节点。完全二叉树是指一个二叉树树除了最底层,其他层都是完全平衡的。


完全二叉树最基本的性质就是它的高度是 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操作


当二叉树的子节点比父节点大时,怎么办呢?没关系,只要按照以下步骤进行就行了:

  1. 将违反规则的节点与父节点进行交换

  2. 重复第一步,直到所有的节点都二叉堆的性质为止

该操作称为swim操作


Sink操作


当二叉树的父节点比任意一个子节点小时,怎么办呢?没关系,只要按照以下步骤进行就行了:

  1. 将违反规则的子节点与两个子节点中较大的子节点进行交换

  2. 重复第一步,直到符合二叉堆的性质为止

该操作称为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();
    }
}