进阶JavaSE-PriorityQueue优先级队列

大家好。好久不见,今天我们接着上次讲完List接口以及List底层所实现的一些容器。今天我们来讲一下Java中的另外一个容器:优先级队列!这个容器,底层是一个,那么具体什么是堆?什么是优先级队列?我们往下看!!!

一、堆的概念

堆,实则就是一颗二叉树的抽象,堆在底层实现,是用一个数组来存储数据的。堆有两种:

  • 大根堆
  • 小根堆

大根堆:在一颗二叉树中,堆顶的元素是整课树中最大的,对于每颗子树而言,也是如此。

小根堆:在一颗二叉树中,堆顶的元素是整棵树中最小的,对于每颗子树而言,也是如此。

image-20211022142032594

上图就是两种堆,在逻辑上是这样的一个形式。那么在具体实现的时候,我们是使用一个数组来存储的,我们又该如何从根节点向下遍历,寻找当前节点的孩子节点呢?

其实,我们在之前的文章中,讲过二叉树的5条性质。(二叉树的概念

二叉树的最后一条性质就是:

image-20211022142525731

也就是说,当前节点的左孩子,等于(i * 2)+ 1,当前节点的右孩子等于(i * 2) + 2。此处的i就是当前节点,也就是在数组中的下标值。(注:因为整棵树的根节点是存储在0下标的位置,所以才推导出以上两个公式)

则可以反推,当前节点的父节点就是(i - 1)/ 2

二、堆的实现

既然我们知道了堆的概念,也知道了怎么在堆上查找当前节点的左右孩子和父节点。现在我们就来实现,堆是怎么保持堆顶的元素是最大(最小的)?

首先我们得清楚一点,堆是怎么进行添加元素的?

在堆上添加元素,其实就是在当前堆的最后面添加元素,可能不是很理解。我们来看图:

image-20211022145931307

也就是说,就像层序遍历一样,在整棵树的最后一层,从左到右,依次插入节点。插入进去之后,我们就需要调整这个节点,看看这个节点是不是大于他的父节点?如果大于的话,如果想调整为大根堆,就需要将这个节点与父节点进行交换数据。反之亦然。

总结:

  • 若想调整为大根堆,若新插入的节点比父节点的数值大,则新插入的节点就需要往上调整。
  • 若想调整为小根堆,若新插入的节点比父节点的数值小,则新插入的节点就需要往上调整。

伪代码:

public void insert(int val) {
    
    
    this.array[最后的位置] = val;
    
    int index = 最后位置;
    int parent = (index - 1) / 2;
    //大根堆
    while (parent >= 0 && array[index] > array[parent]) {
    
     //新插入的节点,大于父节点
        swap(array, index, parent); //交换父节点与新插入节点的数据
        
        //交换之后,然后就更新index的值,继续比较上一层的数据
        index = parent;
        parent = (index - 1) / 2;
    }
}

上面的代码就是怎么在堆上进行插入元素,接下来就是怎么在堆上弹出一个元素?

切记,堆弹出元素,只会弹出当前堆顶的元素,也就是说,会弹出当前这个堆里,最大的值或者最小的值。具体的弹出过程如下:

image-20211022154538276

如上图,堆顶元素与数组的最后一个交换之后,此时14成为了整棵树的堆顶,但是14并不是整棵树的最大值,所以14这个节点,就需要往下调整。调整之后,整棵树的大小需要减少1(下次添加元素的时候,就会插入到现在红色节点(30)处,实现覆盖)。然后返回30即可,这样就是堆的弹出操作。

伪代码:

public int pop() {
    
    
    int index = 当前堆的大小;
    swap(array, 0, index); //堆顶的元素,与数组的最后一个元素进行交换
    this.size--; //堆的大小减1
    heapify(0, size); //将需要被向下调整的下标值,以及堆的大小传入
    return array[index]; //返回交换前的堆顶元素
}

private void heapify(int index, int size) {
    
    
    int leftChild = 2 * index + 1; //index节点的左孩子
    
    while (leftChild < size) {
    
    
        //下面的if,就是判断index的孩子节点,谁是最大的
        if (leftChild + 1 < size && array[leftChild] < array[leftChild+1]) {
    
    
            leftChild++;
        }
        //维护大根堆,index的值更大的话,就不需要调整了
        if (array[index] > array[leftChild]) {
    
    
            break;
        }
        //交换数据
        swap(array, index, leftChild);
        
        index = leftChild;
        leftChild = 2 * index + 1; //继续拿到左孩子的下标,循环
    }
    
}

具体代码如下:

image-20211022160744538

以上两组代码,就是堆的核心,总结起来就是一句话:数组尾部插节点,往上比较做调整。弹出堆顶的元素,与数组尾部做交换,再往下比较做调整。在前期文章中,有一篇排序算法的帖子,写了一个堆排序,可以进去看看堆排序的代码,就是在堆上小小得到改动,即可实现堆排序。八大排序算法

以上全部就是堆的概念和伪代码。

三、优先级队列的使用

在java中,PriorityQueue<>,底层就是一个堆,根据堆的性质,我们可以随时得到当前所有的数据的最大值或者最小值。

image-20211022162020680

PriorityQueue的实例化:

//第一种
PriorityQueue<Integer> heap = new PriorityQueue<>(); //默认是小根堆

//第二种
PriorityQueue<Integer> heap = new PriorityQueue<>(new Comparator<Integer> () {
    
    
    @Override
    public int compare(Integer o1, Integer o2) {
    
    
        return o2 - o1; //右边o2减去左边o1,会得到大根堆
    }
});

以上两种,就是最常用的构造方法,第二种就是给定了相应的比较器,来觉得是大根堆还是小根堆。

切记,java里的优先级队列,不给定比较器,默认的即就是一个小根堆。

接下来,我们来查看一下这个容器,底层是怎么实现的。

无参构造方法

image-20211022163403422

以上的代码,就是优先级队列的无参构造,我们需要注意的是,默认的优先级队列的容量是11。(大家是否还记得,前面讲过,ArrayList的无参构造,默认的是一个空数组,只有添加第一个元素的时候,才会扩容到10,然后就是1.5倍的扩容速度)。

add方法

image-20211022164400038

以上代码,需要记住的是,优先级队列的扩容方法是:当前容量小于64时,是2倍扩容;若大于等于64,就是1.5倍扩容

以上就是PriorityQueue,最重要的概念,可能会被面试问到。

image-20211022170716866

以上红色框中,可能就是作为初学者阶段,最容易用到的方法。

好啦。本期更新就到此结束啦,我们下期见啦!!!

猜你喜欢

转载自blog.csdn.net/x0919/article/details/120909791