目录
- 优先级队列简介
- 关于堆
- 为什么得用完全二叉树
- 用堆来实现优先级队列
- 插入/删除/获取优先级最高的元素
- 模拟实现
- 使用PriorityQueue的注意事项
- PriorityQueue常用接口
- 优先级队列的构造方法
优先级队列简介
PriorityQueue,即优先级队列。它可以保证每次出出来的数据是队列中最大或最小的元素。
JDK1.8中的PriorityQueue底层使用了堆这种数据结构。
关于堆
堆实际就是在完全二叉树的基础上进行了一些调整。
堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
为什么得用完全二叉树
我们来看下图就明白了:
首先我们得明白,堆中数据的存储是用数组来实现的,那么它们的存储结构就出来了:
我们可以看到对于完全二叉树,它可以做到不浪费空间,而非完全二叉树可能会产生很多零碎的空间浪费了,因此我们采用完全二叉树.
将元素存储到数组中后,可以根据二叉树的性质对树进行还原。
假设 i 为节点在数组中的下标,则有:
如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
如果2 * i + 1 小于总的节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
如果2 * i + 2 小于总的节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
用堆来实现优先级队列
先来看看优先级队列的常用方法
插入/删除/获取优先级最高的元素
- boolean offer(E e)
插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度 ,注意:空间不够时候会进行扩容 - E peek()
获取优先级最高的元素,如果优先级队列为空,返回null - E poll()
移除优先级最高的元素并返回,如果优先级队列为空,返回null - int size()
获取有效元素的个数 - void clear()
清空 - boolean isEmpty()
检测优先级队列是否为空,空返回true
模拟实现
public class PriorityQueue {
public int[] elem;
public int usedSize; //记录元素个数
public PriorityQueue() {
elem = new int[11]; //数组初始大小为11
usedSize = 0;
}
/**
* 建堆的时间复杂度:O(n*logn)
*
* @param array
*/
public void createHeap(int[] array) { //将该数组变为优先级队列
elem = array;
usedSize = array.length;
for(int i = (usedSize-2)/2; i >= 0; i--) { //从最后一个元素的父节点开始依次向下调整
shiftDown(i,usedSize);
}
}
/**
*
* @param root 是每棵子树的根节点的下标
* @param len 是每棵子树调整结束的结束条件
* 向下调整的时间复杂度:O(logn)
*/
private void shiftDown(int root,int len) {
int a = root*2 + 1; //a表示左边的子节点
while(a < len) { //向下调整直到a越界了
if(a+1 < len && elem[a] < elem[a+1]) { //如果root有右节点,并且右节点大于左节点,则令a等于右节点
a = a+1; //这里a就表示root的最大子节点
}
if(elem[root] < elem[a]) { //如果最大子节点大于root节点,则两者交换
int tmp = elem[a];
elem[a] = elem[root];
elem[root] = tmp;
root = a; //将root指向交换了的子节点
a = root*2 + 1; //a重新指向root的左子节的
}else {
break; //不大于直接退出循环
}
}
}
/**
* 入队:仍然要保持是大根堆
* @param val
*/
public void push(int val) {
if(isFull()) { //如果满了扩容
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize++] = val; //将待添加的元素放在数组最后面
shiftUp(usedSize-1); //将添加的元素进行向上调整
}
private void shiftUp(int child) {
int root = (child-1) / 2; //找出child的父节点root
while(root >= 0) {
if(elem[child] > elem[root]) { //如果子节点大于父节点,则交换
int tmp = elem[child];
elem[child] = elem[root];
elem[root] = tmp;
child = root; //将子节点指向父节点
root = (child-1) / 2;
}else {
break;
}
}
}
public boolean isFull() {
return usedSize == elem.length;
}
/**
* 出队【删除】:每次删除的都是优先级高的元素
* 仍然要保持是大根堆
*/
public void pollHeap() {
if(isEmpty()) {
throw new RuntimeException("队列为空,无法删除元素");
}
elem[0] = elem[--usedSize]; //删除其实就是将最后一个元素覆盖第一个元素
shiftDown(0,usedSize); //然后对第一个元素进行向下调整
}
public boolean isEmpty() {
return usedSize == 0;
}
/**
* 获取堆顶元素
* @return
*/
public int peekHeap() {
return elem[0];
}
}
这里实现的是大根堆, 其实PriorityQueue底层是小根堆.
使用PriorityQueue的注意事项
- 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
-
PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出
ClassCastException异常. -
不能插入null对象,否则会抛出NullPointerException
-
插入元素时, 若空间不足, 其内部可以自动扩容
可以看到, 如果原数组大小小于64, 则二倍扩容, 大于则1.5倍扩容.
可以看到扩容最大为 Integer.MAX_VALUE 也就是2^31 - 1.
- 插入和删除元素的时间复杂度为: O(logN)
- PriorityQueue底层使用了堆数据结构
- PriorityQueue默认情况下是小堆—即每次获取到的元素都是最小的元素.
PriorityQueue常用接口
优先级队列的构造方法
可以看到有很多构造方法:
这里介绍几个我们常用的构造方法:
默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器.
用到这个构造方法:
// 用户自己定义的比较器:直接实现Comparator接口,然后重写该接口中的compare方法即可
class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1; //这里如果是o1-o2则是小根堆
}
}
public class TestPriorityQueue {
public static void main(String[] args) {
PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
p.offer(4);
p.offer(3);
p.offer(2);
p.offer(1);
p.offer(5);
System.out.println(p.peek());
}
}