进阶JavaSE-PriorityQueue优先级队列
Posted 飞人01_01
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进阶JavaSE-PriorityQueue优先级队列相关的知识,希望对你有一定的参考价值。
大家好。好久不见,今天我们接着上次讲完List接口以及List底层所实现的一些容器。今天我们来讲一下Java中的另外一个容器:优先级队列!这个容器,底层是一个堆,那么具体什么是堆?什么是优先级队列?我们往下看!!!
一、堆的概念
堆,实则就是一颗二叉树的抽象,堆在底层实现,是用一个数组来存储数据的。堆有两种:
- 大根堆
- 小根堆
大根堆:在一颗二叉树中,堆顶的元素是整课树中最大的,对于每颗子树而言,也是如此。
小根堆:在一颗二叉树中,堆顶的元素是整棵树中最小的,对于每颗子树而言,也是如此。
上图就是两种堆,在逻辑上是这样的一个形式。那么在具体实现的时候,我们是使用一个数组来存储的,我们又该如何从根节点向下遍历,寻找当前节点的孩子节点呢?
其实,我们在之前的文章中,讲过二叉树的5条性质。(二叉树的概念)
二叉树的最后一条性质就是:
也就是说,当前节点的左孩子,等于(i * 2)+ 1
,当前节点的右孩子等于(i * 2) + 2
。此处的i就是当前节点,也就是在数组中的下标值。(注:因为整棵树的根节点是存储在0下标的位置,所以才推导出以上两个公式)
则可以反推,当前节点的父节点就是(i - 1)/ 2
。
二、堆的实现
既然我们知道了堆的概念,也知道了怎么在堆上查找当前节点的左右孩子和父节点。现在我们就来实现,堆是怎么保持堆顶的元素是最大(最小的)?
首先我们得清楚一点,堆是怎么进行添加元素的?
在堆上添加元素,其实就是在当前堆的最后面添加元素,可能不是很理解。我们来看图:
也就是说,就像层序遍历一样,在整棵树的最后一层,从左到右,依次插入节点。插入进去之后,我们就需要调整这个节点,看看这个节点是不是大于他的父节点?如果大于的话,如果想调整为大根堆,就需要将这个节点与父节点进行交换数据。反之亦然。
总结:
- 若想调整为大根堆,若新插入的节点比父节点的数值大,则新插入的节点就需要往上调整。
- 若想调整为小根堆,若新插入的节点比父节点的数值小,则新插入的节点就需要往上调整。
伪代码:
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;
}
}
上面的代码就是怎么在堆上进行插入元素,接下来就是怎么在堆上弹出一个元素?
切记,堆弹出元素,只会弹出当前堆顶的元素,也就是说,会弹出当前这个堆里,最大的值或者最小的值。具体的弹出过程如下:
如上图,堆顶元素与数组的最后一个交换之后,此时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; //继续拿到左孩子的下标,循环
}
}
具体代码如下:
以上两组代码,就是堆的核心,总结起来就是一句话:数组尾部插节点,往上比较做调整。弹出堆顶的元素,与数组尾部做交换,再往下比较做调整
。在前期文章中,有一篇排序算法的帖子,写了一个堆排序,可以进去看看堆排序的代码,就是在堆上小小得到改动,即可实现堆排序。八大排序算法
以上全部就是堆的概念和伪代码。
三、优先级队列的使用
在java中,PriorityQueue<>,底层就是一个堆,根据堆的性质,我们可以随时得到当前所有的数据的最大值或者最小值。
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里的优先级队列,不给定比较器,默认的即就是一个小根堆。
接下来,我们来查看一下这个容器,底层是怎么实现的。
无参构造方法:
以上的代码,就是优先级队列的无参构造,我们需要注意的是,默认的优先级队列的容量是11
。(大家是否还记得,前面讲过,ArrayList的无参构造,默认的是一个空数组,只有添加第一个元素的时候,才会扩容到10,然后就是1.5倍的扩容速度)。
add方法:
以上代码,需要记住的是,优先级队列的扩容方法是:当前容量小于64时,是2倍扩容;若大于等于64,就是1.5倍扩容
。
以上就是PriorityQueue,最重要的概念,可能会被面试问到。
以上红色框中,可能就是作为初学者阶段,最容易用到的方法。
好啦。本期更新就到此结束啦,我们下期见啦!!!
以上是关于进阶JavaSE-PriorityQueue优先级队列的主要内容,如果未能解决你的问题,请参考以下文章