java实现(3)-堆

来源:互联网 发布:直播声音软件 编辑:程序博客网 时间:2024/05/19 20:57

引言

堆,我们一般作为二叉堆的一种总称,它是建立在二叉树之上的。在本篇博文中,会详细介绍堆的结构和原理,以至于写出堆的实现。在代码实现中我们主要是针对于插入和删除做一些操作,在删除中我们只考虑删除最小的,而不涉及更深一步的操作。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

场景引入

我们在考虑优先队列的时候会有这样的场景:比如说整个公司都用同一台打印机。一般说来会有队列实现,它遵循FIFO的规则,先提交打印任务的先打印,这无可厚非。但是在实际中,我们希望重要的文件先打印,比如说甲有50页不重要的文件和乙有2页重要文件,甲先提交,这种情况下,我们希望乙能够先打印。FIFO的规则显然不合适。
继续讨论这个问题,如果我们用自定义的链表实现呢?这里可以分为两种情况:
①如果链表是有序的,那么删除最小的元素的时间复杂度是O(1)(如果对时间复杂度不明白的可以查阅相关资料,或者查看我前几篇博文,在这里我详细写过计算方法:http://blog.csdn.net/u012403290/article/details/65631285),但是插入的时间复杂度就是O(N)。
②如果链表是无序的,那么插入定义为插入到最尾部,那么时间复杂度是O(1),但是删除最小的元素时间复杂度就是O(N)。
继续深究一下,如果我用二叉查找树呢?
按照二叉查找树的性质来说,我们插入和删除最小元素的时间复杂度都是O(Log N),相比于链表来说有一定的优化,但是我们要考虑一个问题,频繁的删除最小节点,会导致二叉查找树的退化,也就是说二叉查找树的右子树会比左子树大的多,也有可能会直接退化成链表。

完全二叉树

通俗来说,在除最后一层外,每一层上的节点数均达到最大值,在最后一层上只缺少右边的若干结点。大家可以看下面这张图理解:
这里写图片描述
再说明一下,只能缺少右边的若干节点,并不是可以缺少右子节点。

二叉堆

堆是一颗被完全填满的二叉树,如果没有填满,那么只能是在最后一层,而且在这层上,所有的元素从左到右填入,如果有空缺,只能空缺右边的元素。通俗来说它就是一颗完全二叉树。同时它分为两类描述:
①最小堆
意思就是最小的元素在堆顶,且每一个父节点的值都要小于子节点,下图就是一个最小堆:
这里写图片描述
②最大堆
意思就是最大的在堆顶,且每一个父节点的值都大于子节点,下图就是一个最大堆:
这里写图片描述
我们在代码实现过程中,已最小堆为例。

代码实现

1、描述方式
我们思考二叉堆,发现他不需要用链表来表述,直接用数组就可以表述了,我们尝试把二叉堆从上至下,一层一层平铺成数组。我们把上面的最小堆用数组表示就是:

这里写图片描述

我们对于其进行描述,对于一个二叉堆平铺之后的数组,我们可以发现,任意一个下标元素arr[i],他的左孩子的就是arr[2*i],他的右孩子就是arr[2*i+1],他的父节点就是arr[i/2]。

为什么可以用数组来表述二叉堆?
因为完全二叉树的性质,只能在最后一层的右侧允许缺少节点,而这些节点在数组中处于连续的末端,并不影响前面的所有元素。

2、插入
二叉堆的插入还是很有意思的,一般,我们采用上滤的方式来解决二叉堆的插人:①确认一个可以插入的预插入位置,如果最后一层不满的话,那就插入到最后一层最靠左的那个位置,如果最后一层已满的话,那就新开一层,插入到最左边;②判断把当前数据放入到这个位置是否会破坏二叉堆的性质,如果不破坏,那么插入结束;③如果不符合,那么就需要把这个预插入位置和它的父节点进行兑换;重复②③步骤,直至插入结束。
下面这张图描述了这种插入过程:
这里写图片描述

3、删除
理解了插入的过程,删除其实也不难的。想对应的,我们称这种方法为下滤。在最小堆中,我们知道如果要删除最小的,那么其实就是删除堆顶就可以了。可想而知,那我们删除之后,有必要把整个二叉堆恢复到满足的条件。也就是说:①移除堆顶元素。并指定当前位置为预插入位置,并尝试把最后一个元素(最后一个元素在二叉堆的最后一层的最后一个位置)放到这个②如果不能顺利插入,那么就比较它的孩子,把较小的孩子放入这个预插入位置。③继续处理这个预插入位置,循环②步骤,直至又形成一个完整的二叉堆位置。
下面这张图描述了这种删除最小的过程:
这里写图片描述

代码实现

以下是用代码实现的二叉堆,包含了初始化,插入和删除:

package com.brickworkers;public class Heap<T extends Comparable<? super T>> {    private static final int DEFAULT_CAPACITY = 10; //默认容量    private T[] table; //用数组存储二叉堆    private int size; //表示当前二叉堆中有多少数据    public Heap(int capactiy){        this.size = 0;//初始化二叉堆数据量        table = (T[]) new Comparable[capactiy + 1];//+1是因为我们要空出下标为0的元素不存储    }    public Heap() {//显得专业些,你就要定义好构造器        this(DEFAULT_CAPACITY);    }    //插入    public void insert(T t){        //先判断是否需要扩容        if(size == table.length - 1){            resize();        }        //开始插入        //定义一个预插入位置下标        int target = ++size;        //循环比较父节点进行位置交换        for(table[ 0 ] = t; t.compareTo(table[target/2]) < 0; target /= 2){            table[target] = table[target/2];//如果满足条件,那么两者交换,知道找到合适位置(上滤)        }        //插入数据        table[target] = t;        print();    }    //删除最小    //删除过程中,需要重新调整二叉堆(下滤)    public void deleteMin(){        if(size == 0){            throw new IllegalAccessError("二叉堆为空");        }        //删除元素        table[1] = table[size--];        int target  = 1;//从顶部开始重新调整二叉堆        int child;//要处理的节点下标        T tmp = table[ target ];        for( ; target * 2 <= size; target = child )        {            child = target * 2;            if( child != size &&table[ child + 1 ].compareTo( table[ child ] ) < 0 ){//如果右孩子比左孩子小                child++;            }            if( table[ child ].compareTo( tmp ) < 0 ){                table[ target ] = table[ child ];                table[child] = null;            }            else{                 break;            }        }        table[ target ] = tmp;        print();    }    //如果插入数据导致达到数组上限,那么就需要扩容    private void resize(){          T [] old = table;          table = (T [])  new Comparable[old.length*2 + 1];//把原来的数组扩大两倍          for( int i = 0; i < old.length; i++ )              table[ i ] = old[ i ];        //数组进行拷贝    }    //打印数组    private void print(){        System.out.println();        for (int i = 1; i <= size; i++) {            System.out.print(table[i] + " ");        }        System.out.println("二叉堆大小:"+size);    }    public static void main(String[] args) {        Heap<Integer> heap = new Heap<>();        //循环插入0~9的数据        for (int i = 0; i < 10; i++) {            heap.insert(i);        }        //循环删除3次,理论上是删除0,1,2         for (int i = 0; i < 3; i++) {            heap.deleteMin();        }    }}//输出结果:////0 二叉堆大小:1////0 1 二叉堆大小:2////0 1 2 二叉堆大小:3////0 1 2 3 二叉堆大小:4////0 1 2 3 4 二叉堆大小:5////0 1 2 3 4 5 二叉堆大小:6////0 1 2 3 4 5 6 二叉堆大小:7////0 1 2 3 4 5 6 7 二叉堆大小:8////0 1 2 3 4 5 6 7 8 二叉堆大小:9////0 1 2 3 4 5 6 7 8 9 二叉堆大小:10////1 3 2 7 4 5 6 9 8 二叉堆大小:9////2 3 5 7 4 8 6 9 二叉堆大小:8////3 4 5 7 9 8 6 二叉堆大小:7

尾记

这里,对于新手来说有一个小小的规则。对于一个类,代码模块存放顺序一般都是:静态成员变量/常量,构造方法,public方法,private方法。我主要说的是要把你封装起来的private方法放到最后面,因为别人查看你的代码的时候,别人希望最先看到的是你暴露出来的public方法,而不是对他来说无关紧要的private方法。

希望对你有所帮助。

2 0