• 堆(Java) --优先级队列的代理者
  • 最大堆


堆(Java) --优先级队列的代理者

  聊堆不能不聊优先级队列,优先级队列就是决定哪个任务优先执行的队列,通常会有一个优先级的数据,通过数据的大小来判断优先级,实现优先级队列其实有三种方式:

  • 第一种:无序数组队列,这种在入队时的时间复杂度为O(1),但是出队时的时间复杂度是O(n);
  • 第二种:有序数组队列,这种在入队时的时间复杂度为O(n),但是出队时的时间复杂度是O(1);
  • 第三种:堆,最大堆或最小堆,入队出队都是O(logn),虽然相较于第一种第二种时会在入队或出队时较慢,但是当频繁进行操作,数据量大时,动态进行时,堆所需要的平均时间远远少于第一种和第二种

  通过上面三种优先级队列的比较就体现出了堆的优势,所以称堆为优先级队列的代理者,另外需要意识到的是,堆有两种基本形态最大堆和最小堆,而不管是哪种形态其本质都是一棵树,而且是一棵完全二叉树,最大堆的性质是父节点的值一定大于字节点,最小堆则是父节点的值一定小于子节点,兄弟节点大小无所谓~~
  以下演示的是一个最大堆的实现,对于最大堆的存储用的还是一个数组,只是这个数组中的元素有些讲究;元素的讲究主要体现在对这个堆数组插入(insert方法)元素和弹出根节点元素(popTheTop)的实现上:

最大堆

// ELE继承Comparable,方便比较
public class MaxHeap<ELE extends Comparable> implements Heap {

    private ELE[] data;
    private int size;
    private int cap;    // 容量

    // 基本构造函数,默认最大堆中数据为空
    public MaxHeap(int capacity) {
        data = (ELE[])new Comparable[capacity+1];
        size = 0;
        cap = capacity;
    }

    // 将数组最大堆化的构造函数
    public MaxHeap(ELE[] arr) {
        int n = arr.length;

        data = (ELE[])new Comparable[n+1];
        size = n;
        cap = n;

        for(int i=0; i<n; i++) {
            data[i+1] = arr[i];
        }

        // 最大堆化
        for(int i=n/2; i>=1; i--)
            spinDown(i);
    }

    // 插入元素
    public void insertELE(ELE ele) {
        // 元素个数已满则直接返回
        if(size >= cap)
            return;

        data[++size] = ele;
        spinUp(size);
    }

    // 弹出最大元素
    public ELE popMax() {
        ELE BigBrother = data[1];

        // 最后的小弟与老大交换,老大已弹出,当容量减一,这个数据就被忽略了,所以无需清理掉这个数据
        // 减一直接用后--进行
        UtilsSet.swap(data,1,size--);
        // 自上而下恢复最大堆
        spinDown(1);

        // 再见大兄弟
        return BigBrother;
    }

    // 内部堆化工具 -- 插入数据后用,自底向上找到合适位置,传入的upward即为底
    private void spinUp(int upward) {
        while(upward/2 > 0 && data[upward].compareTo(data[upward/2]) > 0) {
            UtilsSet.swap(data, upward, upward / 2);
            upward /= 2;
        }
    }

    // 内部堆化工具 -- 自顶向下找到合适位置,可用于弹出数据后,从底调上数据后用,也可用于对随机数组堆化,传入的superStar位置不同
    private void spinDown(int superStar) {
        // 记录临时小哥,这个临时小哥为目前自顶向下的临时老大
        ELE tempBoss=data[superStar];
        // 临时小哥相当老大必须比自己的左右小弟节点大,因可能没有左右小弟,所以作为循环介绍与否的判断
        while(superStar*2 <= size) {
            int bigGuy = whoIsTheBigGuy(superStar*2, superStar*2+1);
            // 如果临时小哥是凭实力当的老大,别搞事情了,结束战斗
            if(tempBoss.compareTo(data[bigGuy]) >= 0)
                break;
            // 如果临时小哥没两个小弟中更大的牛,则需要让位
            // 临时小哥的位置可以更新,数据已经记在tempBoss中了,继续进行循环,直到临时小哥找到属于自己的小弟,然后再将tempBoss中的值赋值
            data[superStar] = data[bigGuy];
            superStar = bigGuy;
        }
        data[superStar] = tempBoss;
    }

    // 找到同老大的左右两个小弟节点中较大的序号,并返回
    private int whoIsTheBigGuy(int left, int right) {
        if(data[right] == null || data[left].compareTo(data[right]) >= 0)
            return left;
        else
            return right;
    }

    @Override
    public int getSize() {

        return size;
    }

    @Override
    public boolean isEmpty() {

        return size == 0;
    }

    // 测试用
    public static void main(String[] args) {
        MaxHeap<Integer> maxHeap = new MaxHeap<Integer>(100);
        int N = 100; // 堆中元素个数
        int M = 100; // 堆中元素取值范围[0, M)
        for( int i = 0 ; i < N ; i ++ )
            maxHeap.insertELE( Integer.valueOf((int)(Math.random() * M)) );

        Integer[] arr = new Integer[N];
        // 将maxheap中的数据逐个取出来
        // 取出来的顺序应该是按照从大到小的顺序取出来的
        for( int i = 0 ; i < N ; i ++ ){
            arr[i] = maxHeap.popMax();
            System.out.print(arr[i] + " ");
        }
        System.out.println();

        // 确保arr数组是从大到小排列的
        for( int i = 1 ; i < N ; i ++ )
            assert arr[i-1] >= arr[i];
    }

}

  最小堆的实现大同小异,需要注意的是这里的最大堆实现为了方便是从数组下标1开始,而不是0开始