完全二叉树——二叉堆(BinaryHeap)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/swpu_ocean/article/details/83213995

前言

优先队列是允许至少下列两种操作的数据结构:insert(插入)以及deleteMin(删除最小者),其中deleteMin的工作是找出、返回、并删除优先队列中最小的元素。insert操作等价于enqueue(入队),而deleteMin则相当于dequeue(出队)。

二叉堆的性质

二叉堆的使用对于优先队列的实现相当普遍。二叉堆具有结构性和堆序性:

结构性质

堆是一棵被完全填满的二叉树,有可能的例外是在底层叶子上,叶子上的元素从左到右填入。这样的树称为完全二叉树。

根据完全二叉树的性质,我们可以使用一个数组来表示而不需要使用链。该数组有一个位置0,可在进行堆的插入操作时避免多次的赋值(《数据结构forJava版》)。

对于数组中任一位置i上的元素,其左儿子在位置2i上,右儿子在左儿子后的节点(2i+1)上,它的父亲则在位置i/2上。因此,这里不需要链就可以很简单的遍历该树。

一个堆结构将由一个(Comparable对象的)数组和一个代表当前堆的大小的整数组成。

堆序性质

**在一个堆中,对于每一个节点X,X的父亲中的关键字小于或等于X中的关键字,根节点除外(它没有父亲)。**根据堆序性质我们可以很容易的得出最小的元素一定在根上,因此快速找出最小元将会是件非常容易的事,并且只需要花费常数时间。

基本的堆操作

在对堆的操作中,无外乎是需要往堆中插入元素以及删除最小元素,但是所有的操作都需要保证始终保持堆序性质。

insert插入

为了将一个元素X插入到堆中,我们需要在下一个可用位置创建一个空穴,否则该堆将不是完全树。如果X可以放在该空穴中而不破坏堆的序,那么插入完成。否则,我们把空穴的父节点上的元素移入该空穴中,这样,空穴就朝着根的方向上冒一步。继续该过程直到X能被放入空穴中为止。

根据下图,我们想要插入14,我们在堆的下一个可用位置创建一个空穴,由于将14插入空穴破坏了堆的序(空穴父亲的关键字31大于14),因此将31移入该空穴,空穴位置上移到原31位置,接着继续比较现在空穴与其父节点,直到找到置入14的正确位置。

堆的插入策略叫做上滤。新元素在堆中上滤直到找到正确的位置。代码也很容易实现插入。

/**
     * 插入操作,需要上滤元素
     * 通过不断比较空穴元素hole与其父元素的大小进行元素上滤
     */
    public void insert(T item){
        if(currentSize == array.length - 1){
            enlargeArray(array.length * 2 + 1);
        }
        int hole = ++currentSize;
        for(;hole > 1 && item.compareTo(array[hole/2]) < 0;hole /= 2){
            array[hole] = array[hole/2];
        }
        array[hole] = item;
    }

当进行元素插入时,由于我们是使用数组作为堆的元素存放,因此必须考虑数组越界问题,其中currentSize为数组此刻的最后一个元素的序号,当它大于数组长度时需要进行扩容。

我们通过array[hole/2]获得节点的父节点,然后来更改堆的序,确保每一个结点的父亲的关键字都要小于或等于该节点。

正常的一次交换操作需要执行三条赋值语句,如果一个元素上滤d层,那么由于交换而执行的赋值次数就达到3d,而我们这里的方法却只用到d+1次赋值。

deleteMin删除最小元

deleteMin以类似插入的方式处理。找出最小元是容易的,困难之处是删除它。当删除一个最小元时,需要在根节点建立一个空穴。由于现在堆少了一个元素,因此堆中最后一个元素X必须移动到该堆的某个地方。如果X可以被放到空穴中,那么deleteMin完成,不过这一般不太可能,因此我们将空穴的两个儿子中较小者移入空穴,这样空穴向下推了一层。重复该步骤直到X可以被放入空穴中。

在下图中,我们删除了该堆的最小元13,因此13位置成了空穴,所有此时需要将堆的最后一个元素31移入该空穴,但是发现该空穴的左儿子14小于该元素,因此需要将该左儿子移入空穴,并且空穴下移,反复该操作,直到31被放入正确的位置。这种一般的策略叫做下滤

删除最小元的代码如下:

/**
     * 删除最小元
     * 将堆中最后一个元素放在根节点
     * 然后进行元素下滤
     * @return
     */
    public T deleteMin(){
        if (isEmpty()){
            throw new NoSuchElementException();
        }
        T minItem = findMin();
        array[1] = array[currentSize];
        array[currentSize--] = null;
        percolateDown(1);
        return minItem;
    }

    /**
     * 元素下滤
     * 从根节点开始比较其左右儿子,找出最小的儿子与父亲进行比较
     * 直到两儿子的值都大于父亲则结束循环
     * 最后将根节点的值放入最小的儿子处
     * @param hole
     */
    public void percolateDown(int hole){
        int child;
        T tmp = array[hole];
        for(;hole * 2 <= currentSize;hole = child){
            child = hole * 2;
            if(child != currentSize && array[child + 1].compareTo(array[child]) < 0){
                child++;
            }
            if(child != currentSize && array[child].compareTo(tmp) < 0){
                array[hole] = array[child];
            } else {
                break;
            }
        }
        array[hole] = tmp;
    }

由于我们必须保证节点不总存在两个儿子,因此在第30行我们需要对节点的儿子进行大小比较,确保下滤元素总是流向较小的一方。

完整代码

由于堆的插入和删除最小元的代码已经在上面给出,因此下面的代码段将省略这两个方法的代码。

/**
 * @author: zhangocean
 * @Date: 2018/10/19 13:19
 * Describe:
 */
public class BinaryHeap<T extends Comparable<? super T>> {

    private static final int DEFAULT_CAPACITY = 10;
    private int currentSize;
    private T[] array;

    public BinaryHeap() {
        this(DEFAULT_CAPACITY);
    }

    @SuppressWarnings("unchecked")
    private BinaryHeap(int heapSize) {
        currentSize = 0;
        //不能使用泛型创建数组
        array = (T[]) new Comparable[heapSize + 1];
    }

    @SuppressWarnings("unchecked")
    public BinaryHeap(T[] items) {
        currentSize = items.length;
        array = (T[]) new Comparable[(currentSize + 2) * 11 / 10];
        int i = 1;
        for (T item : items) {
            array[i++] = item;
        }
        buildHeap();
    }

    /**
     * 建立堆序
     * 从叶子节点中最左边的父节点开始进行元素下滤
     */
    private void buildHeap() {
        for (int i = currentSize / 2; i > 0; i--) {
            percolateDown(i);
        }
    }

    /**
     * 插入操作,需要上滤元素
     * 通过不断比较空穴元素hole与其父元素的大小进行元素上滤
     */
    public void insert(T item) {
        ///插入代码见上方
    }

    /**
     * 查找堆中最小的元素(即根上的元素)
     *
     * @return
     */
    public T findMin() {
        if (isEmpty()) {
            return null;
        }
        return array[1];
    }

    /**
     * 删除最小元
     * 将堆中最后一个元素放在根节点
     * 然后进行元素下滤
     *
     * @return
     */
    public T deleteMin() {
        ///删除最小元代码见上方
    }

    /**
     * 元素下滤
     * 从根节点开始比较其左右儿子,找出最小的儿子与父亲进行比较
     * 直到两儿子的值都大于父亲则结束循环
     * 最后将根节点的值放入最小的儿子处
     *
     * @param hole
     */
    public void percolateDown(int hole) {
        ///元素下滤代码见上方
    }


    public boolean isEmpty() {
        return currentSize == 0;
    }

    public void makeEmpty() {
        currentSize = 0;
        for (int i = 0; i < array.length; i++) {
            array[i] = null;
        }
    }

    /**
     * 数组扩容
     */
    private void enlargeArray(int newArraySize) {
        T[] oldArray = array;
        array = (T[]) new Comparable[newArraySize];
        int i = 0;
        for (T item : oldArray) {
            array[i++] = item;
        }
    }

    public void printHeap() {
        for (T item : array) {
            System.out.print(item + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        BinaryHeap<Integer> heap = new BinaryHeap<Integer>();
        for (int i = 0; i < 20; i++) {
            heap.insert(i);
        }
        heap.printHeap();
        heap.deleteMin();
        heap.printHeap();
        heap.deleteMin();
        heap.deleteMin();
        heap.printHeap();
    }
}

输出结果如下所示:

null 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 null null 
null 1 3 2 7 4 5 6 15 8 9 10 11 12 13 14 19 16 17 18 null null null 
null 3 4 5 7 9 11 6 15 8 17 10 18 12 13 14 19 16 null null null null null 

总结

  1. 二叉堆对于优先队列的实现相对较于普遍。
  2. 堆需要保持堆的序,即对于堆中每个节点X,X的父亲中关键字小于或等于X中的关键字,当然啦根节点除外(它木有父亲)。
  3. 二叉堆是一棵完全二叉树,根据它的结构性可以用数组的方式来实现,而避免了链的使用。
  4. 由于是用数组来实现一棵树结构,因此需要能够对树中节点进行比较,所以通过newComparable数组实现数组元素之间的比较。
  5. 堆的主要操作在于元素插入以及删除最小元,插入时先在最后一个节点的下一个位置建立一个空穴,然后试着将需要插入的元素放入,否则上滤元素。删除最小元则是移除根节点(不用说,它肯定最小)元素,根节点成为空穴,再将最后一个结点试着移入该空穴中,否则进行元素下滤操作。

更多文章请关注我的个人博客:www.zhyocean.cn

猜你喜欢

转载自blog.csdn.net/swpu_ocean/article/details/83213995