实现优先队列的基本数据结构是使用二叉堆

一、二叉堆

二叉堆即用一颗完全二叉树(底层元素从左向右填入)。

java 队列 如何实现 先进先出 java优先级队列实现大堆_java


如果所示,因此我们可以使用一个数组来表示二叉堆,而不使用链表:

0   1    2    3    4    5    6    7     8     9   10
    A    B    C    D    E    F    G     H     I

可以发现,对于任意节点 i,其 左二子在 2i 位置,右儿子在 2i+1位置

关于优先队列,我们可以规定最大元优先或最小元优先,对应的堆就是大顶堆和小顶堆。
大顶堆: 即树上每个节点都小于等于其父亲节点,除了根节点,因为根节点没有父亲
小顶堆:即树上每个节点都大于等于其父亲节点。同上

二、基本的堆操作(以小顶堆为例)

1.插入

1.可以在下一个可用位置处创建一个空节点,将待插入节点x放在0位置(0位置为空)
2.然后循环比较待加入节点和空节点的父亲比较,如果父节点大于x,则将让父节点置于空节点,空节点上冒到原父节点;如果父节点小于x,则直接将x放在空位置,此时不会改变堆序

若创建大顶堆,则和父亲节点比较时,同小顶堆相反即可。

实现如下:
先看基本的数据结构:

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);
    }

    public BinaryHeap(int capacity) {
        this.currentSize = 0;
        array = (T[]) new Comparable[capacity + 1];
    }
}

插入操作:

/**
     * 先建立一个空位置 hole,上滤直到找到满足堆序的位置,将x插入到该位置
     * array[0]暂存待插入元素x
     * @param x
     */
    public void insert(T x) {
    	//扩容操作
        if (currentSize == array.length - 1) {
            enlargeArray(array.length * 2 + 1);
        }
        //让下一个节点为空节点
        int hole=++currentSize;
        //将待插入节点放在0处
        array[0]=x;
        //从空节点开始上冒
        percolateUp(hole,array[0]);
    }
   /**
     * 上冒
     * @param hole
     */
    private void percolateUp(int hole,T x){
        for ( ;  x.compareTo(array[hole/2])<0 ;hole /= 2) {
            array[hole]=array[hole/2];
        }
        array[hole]=x;
    }
2. 删除最小值(出队)

在二叉堆中,最小值即第arr[1]元素,但是删除它后,根节点变为空,因此我们需要把最后一个节点X放在这个空元素上,同时让当前堆大小减一,如果整个堆有序,则直接返回最小值,但是显然不可能保持堆序。
因此我们就需要从该空节点开始进行下滤操作:
下滤与上冒相反,就是沿着空节点开始包含最小儿子的路径上找到一个位置可以放入X。

图例:删除最小节点A

java 队列 如何实现 先进先出 java优先级队列实现大堆_堆排序_02


java 队列 如何实现 先进先出 java优先级队列实现大堆_java 队列 如何实现 先进先出_03


B比I小,因此将空节点下滤:

java 队列 如何实现 先进先出 java优先级队列实现大堆_队列_04


D比I小,因此空节点移动到D位置:

java 队列 如何实现 先进先出 java优先级队列实现大堆_java 队列 如何实现 先进先出_05


此时I放在当前的空节点位置可以保持堆序:

java 队列 如何实现 先进先出 java优先级队列实现大堆_数据结构_06


实现如下:

/**
     * 删除最小元素,下滤
     * 1.找到最小元素,移除(currentSize--,并将最后一个元素放在最小元素位置)
     * 2.对该元素进行下滤
     * @return
     */
    public T deleteMin(){
        if (isEmpty()){
            System.out.println("堆为空");
        }
        T min = findMin();
        array[1]=array[currentSize--];
        percolateDown(1);
        return min;

    }

    /**
     * 从hole开始下滤
     * @param hole
     */
    private void percolateDown(int hole) {
        int child;
        T temp = array[hole];
        for ( ; hole*2<=currentSize ;hole=child ) {
            child=hole*2;
            if (child!=currentSize&& //偶数个节点的情况下,最后一个hole只有一个左儿子,如果没有此条件array[child+1]会出现空指针异常
                array[child+1].compareTo(array[child])<0){//右儿子比左儿子还小
                child++;//child指向右儿子
            }
            //如果temp大于两个儿子中最小的一个节点,则让空节点下移到child
            if (array[child].compareTo(temp)<0){
                array[hole]=array[child];
            }else
                break;
        }
        array[hole]=temp;
    }


    public T findMin(){
        if (isEmpty()){
            System.out.println("堆为空");
        }
        return array[1];
    }

完整代码:

/**
 * 堆(优先队列)
 *
 * @author MaoLin Wang
 * @date 2020/2/1611:26
 */
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);
    }

    public BinaryHeap(int capacity) {
        this.currentSize = 0;
        array = (T[]) new Comparable[capacity + 1];
    }

    public BinaryHeap(T[] array) {
        currentSize = array.length;
        this.array = (T[]) new Comparable[(currentSize + 2) * 11 / 10];
        int i = 1;
        for (T arr : array) {
            array[i++] = arr;
        }
        buildHead();
    }

    private void buildHead() {
        for (int i = currentSize/2; i >0 ; i--) {
            percolateDown(i);
        }
    }

    private void enlargeArray(int newSize) {
        T[] old = array;
        array = (T[]) new Comparable[newSize];
        for (int i = 0; i < old.length; i++) {
            array[i] = old[i];
        }
    }

    /**
     * 先建立一个空位置 hole,上滤直到找到满足堆序的位置,将x插入到该位置
     * array[0]暂存待插入元素x
     * @param x
     */
    public void insert(T x) {
        if (currentSize == array.length - 1) {
            enlargeArray(array.length * 2 + 1);
        }
        int hole=++currentSize;
        array[0]=x;
        percolateUp(hole,array[0]);
    }


    /**
     * 上滤
     * @param hole
     */
    private void percolateUp(int hole,T x){
        for ( ;  x.compareTo(array[hole/2])<0 ;hole /= 2) {
            array[hole]=array[hole/2];
        }
        array[hole]=x;
    }

    /**
     * 删除最小元素,下滤
     * 1.找到最小元素,移除(currentSize--,并将最后一个元素放在最小元素位置)
     * 2.对该元素进行下滤
     * @return
     */
    public T deleteMin(){
        if (isEmpty()){
            System.out.println("堆为空");
        }
        T min = findMin();
        array[1]=array[currentSize--];
        percolateDown(1);
        return min;

    }

    /**
     * 从hole开始下滤
     * @param hole
     */
    private void percolateDown(int hole) {
        int child;
        T temp = array[hole];
        for ( ; hole*2<=currentSize ;hole=child ) {
            child=hole*2;
            if (child!=currentSize&& //保证偶数个节点的情况下,最后一个hole只有一个左儿子
                array[child+1].compareTo(array[child])<0){
                child++;
            }
            if (array[child].compareTo(temp)<0){
                array[hole]=array[child];
            }else
                break;
        }
        array[hole]=temp;
    }


    public T findMin(){
        if (isEmpty()){
            System.out.println("堆为空");
        }
        return array[1];
    }
    public void print(){
        for (int i = 1; i <currentSize ; i++) {
            System.out.println(array[i]);
        }
    }

    private boolean isEmpty() {
        return currentSize==0;
    }
    // Test program
    public static void main( String [ ] args )
    {
        BinaryHeap<Integer> heap = new BinaryHeap<>();
        heap.insert(13);
        heap.insert(14);
        heap.insert(16);
        heap.insert(19);
        heap.insert(21);
        heap.insert(19);
        heap.insert(68);
        heap.insert(65);
        heap.insert(26);
        heap.insert(32);
        heap.insert(31);
        System.out.println("-----");
        while (!heap.isEmpty()){
            //依此出队
            System.out.println(heap.deleteMin());
        }

    }
}

结果:
13
14
16
19
19
21
26
31
32
65
68

大顶堆的下滤操作也基本相同,只是改变一个比较的符号。

三、堆排序

讲完了优先队列的实现后,再实现堆排序就很容易了。
进行优先队列时,我们构造了大顶堆或小顶堆。这里依然以小顶堆为例:
1.如果我们想让一个小顶堆数组变成有序数组,只需从最后一个元素开始,与根节点交换(相当于在优先队列中删除最小值,但是这里把最小值保存到了最后一个元素的位置),同时让currentSize减一。
2.对新的根节点进行下滤操作,直到currentSize
3.这样循环直到交换完所有元素,就得到了一个从大到小的有序数组

实现如下:

/**
     * 使用小顶堆 --->从大到小排序
     * @param arr
     */
    public static void heapSort(int[] arr){
        for (int i = arr.length/2-1; i >=0 ; i--) {
            //从 arr.length/2-1 即倒数第一个非叶子节点开始,逐一下滤,形成小顶堆
            perDownMin(arr,i,arr.length);
        }
        for (int i = arr.length-1; i >0 ; i--) {
            //将根节点,即最小的节点与最后一个节点交换,让最小值逐一放在最后
            swap(arr,i,0);
            //从根节点开始重新生成小顶堆
            perDownMin(arr,0,i);
        }
    }
     /**
     * 小顶堆下滤
     * @param arr
     * @param i
     * @param length
     */
    private static void perDownMin(int[]arr, int i, int length){
        int child;
        int temp;

        for (temp=arr[i];leftChild(i)<length ;i=child) {
            child=leftChild(i);
            if (leftChild(i)!=length-1&&arr[child+1]<arr[child]){
                child++;
            }
            if (temp>arr[child]){
                arr[i]=arr[child];
            }else {
                break;
            }
        }
        arr[i]=temp;
    }
    private  static  void swap(int[] arr, int index1, int index2){
        int temp=arr[index1];
        arr[index1]=arr[index2];
        arr[index2]=temp;
    }

需要注意的是,这里不需要像优先队列进行删除(出队)和插入(入队)操作,因此数组是从0开始而不是从1开始,在个别判断有所不同。

同样的方法以大顶堆实现堆排序:

/**
     * 使用大顶堆 ----> 从小到大排序
     * @param arr
     * @param <T>
     */
    public static <T extends Comparable<? super T>>void  heapSort(T[] arr){
        for (int i = arr.length/2-1; i >=0 ; i--) {
            perDownBig(arr,i,arr.length);
        }
        for (int i = arr.length-1; i >0 ; i--) {
            swap(arr,0,i);
            perDownBig(arr,0,i);
        }
    }
    /**
     * 大顶堆下滤
     * @param arr
     * @param i
     * @param length
     * @param <T>
     */
    private static <T extends Comparable<? super T>>void perDownBig(T[] arr, int i, int length){
        int child;
        T temp;
        for(temp=arr[i];leftChild(i)<length;i=child){
            child=leftChild(i);
            if (child!= length-1 && arr[child].compareTo(arr[child+1])<0){
                child++;
            }
            if (temp.compareTo(arr[child])<0){
                arr[i]=arr[child];
            }else {
                break;
            }
        }
        arr[i]=temp;

    }

leftChild是一个私有的计算孩子节点位置的方法:

private static int leftChild(int i){
        return 2*i+1;
    }

测试:

public static void main(String[] args) {
/*
        int[] array =new int[8000000];
        for (int i=0;i<8000000;i++){
            array[i]=(int)(Math.random()*8000000);
        }

        long begintime=System.currentTimeMillis();

        heapSort(array);
             long endtime=System.currentTimeMillis();
     System.out.println("用时:"+(endtime-begintime)+"ms");*/
        int[] array =new int[10];
        for (int i=0;i<10;i++){
            array[i]=(int)(Math.random()*100);
        }
        heapSort(array);
        for (int i:array){
            System.out.println(i);
        }
    }
97
77
72
69
65
43
32
19
17
5

堆排序的时间复杂度为 NlogN
空间复杂度为 1