之前我们学习过队列是一种先进先出的数据结构,但在某些场景下,操作的数据具有优先级,在出队列时,需要优先级高的数据先出队列。比如在打游戏时来电话了,那么操作系统应该先处理电话;还有在游戏英雄联盟中泰坦的Q的优先级比机器人高;
诸如这些情况下,我们需要数据结构提供两种最基本的操作,首先添加新的对象,然后返回最高优先级对象,这种数据结构就是优先级队列(Priority Queue).
在JDK1.8中的PriorityQueue底层使用了堆的数据结构,而堆通常是一个可以被看做一棵完全二叉树的数组对象。
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆
堆满足以下性质:
因为堆是一颗完全二叉树,所以我们可以利用层序遍历的方式来存储元素
注:对应非完全二叉树则不适用于顺序方式来存储,因为为了通过顺序表来还原二叉树,则空间中就需要存储空结点,这样会导致空间利用率低。
当元素存储到数组中后,我们如何还原二叉树呢?假设i为节点在数组中的下标,
用代码来表示就是:
parent = i / 2;
leftChild = 2 * i;
rightChild = 2 * i + 1;
//或者已知父节点为root
parent = root;
leftChild = (2 * parent) + 1;
rightChild = (2 * parent) + 1;
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如何将其创建成堆呢?
通过观察可知:根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。
建小堆为例:
- 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
- 如果parent的左孩子存在,即:child < len, 进行以下操作,直到parent的左孩子不存在或者已经满足堆的性质。
2.1 parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标记
2.2 将parent与较小的孩子child比较,如果parent小于较小的孩子child,调整结束;否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后重复2 。
/**
* 向下调整 -
* 时间复杂度 0(logN)
* @param arr 二叉树中存放的元素
* @param root 根节点的下标
* @param len 数组长度
*/
private static void shiftDown(int[] arr, int root, int len) {
int parent = root;
int child = (2 * parent) + 1;//左孩子
//孩子节点索引不会大于len
while (child < len) {
//使child索引位置保存子节点的最大值
if (child + 1< len && arr[child] < arr[child + 1]) {
child++;
}
if (arr[child] > arr[parent]) {
swap(arr, child, parent);
parent = child;
child = (2 * parent) + 1;
} else {
break;
}
}
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
对于一个普通序列,即根的左右子树都不满足堆的特性,那么该如何调整呢?
/**
* 建堆
* 时间复杂度 0(n)
* @param arr
*/
private static void createHeap(int[] arr) {
// 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
for (int p = (arr.length - 1 - 1) / 2; p >=0; p--) {
shiftDown(arr, p, arr.length);
}
}
插入步骤:
/**
* 向堆中插入元素
* @param val
*/
public void push(int val) {
//判满
if (ifFull()) {
this.elem = Arrays.copyOf(elem, elem.length * 2);
}
//插入
this.elem[usedSize] = val;
//调整
shiftUp(usedSize);
//有效数据调整
usedSize++;
}
private boolean ifFull() {
return elem.length == usedSize;
}
/**
* 向上调整 - 向堆中添加元素 - 只能添加到末尾然后向上调整
*/
private void shiftUp(int child) {
int parent = (child - 1) / 2;
while (child > 0) {
//这里本来就是小根堆,根节点的值一定比原来的左右子结点小,所以不需要比较左右节点的大小
if (elem[child] > elem[parent]) {
swap(elem, child, parent);
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
删除步骤:
注:堆的删除一定是删除优先级高的元素(即堆顶元素)
/**
* 出堆顶元素
*/
public void pollHeap() {
if (isEmpty()) {
System.out.println("队列为空");
return;
}
//把堆顶元素和堆尾元素交换
swap(elem, 0, usedSize - 1);
//调整大小
usedSize--;
//堆的结构改变,所以要向下调整
shiftDown(elem, 0, usedSize);
}
/**
* 向下调整 -
* 时间复杂度 0(logN)
* @param arr 二叉树中存放的元素
* @param root 根节点的下标
* @param len 数组长度
*/
private static void shiftDown(int[] arr, int root, int len) {
int parent = root;
int child = (2 * parent) + 1;//左孩子
//孩子节点索引不会大于len
while (child < len) {
//使child索引位置保存子节点的最大值
if (child + 1< len && arr[child] < arr[child + 1]) {
child++;
}
if (arr[child] > arr[parent]) {
swap(arr, child, parent);
parent = child;
child = (2 * parent) + 1;
} else {
break;
}
}
}
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,我们主要介绍PriorityQueue。
使用注意事项:
- PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
- 不能插入null对象,否则会抛出NullPointerException
- 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
- 插入和删除元素的时间复杂度为0(logN)
- PriorityQueue底层使用了堆数据结构, 且是最小堆
它的构造方法在JDK1.8中一共有7种,下面我们介绍几种常见的构造方法
构造器 | 功能介绍 |
---|---|
PriorityQueue() | 创建一个空的优先级队列,默认容量是11 |
PriorityQueue(int initialCapacity) | 创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛IllegalArgumentException异常 |
PriorityQueue(Collection extends E> c) | 用一个集合来创建优先级队列 |
PriorityQueue(Comparator super E> comparator) | 设置自己的比较器 |
public static void main(String[] args) {
// 创建一个空的优先级队列,底层默认容量是11
PriorityQueue<Integer> q1 = new PriorityQueue<>();
// 创建一个空的优先级队列,底层的容量为initialCapacity
PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
ArrayList<Integer> list = new ArrayList<>();
list.add(4);
list.add(3);
list.add(2);
list.add(1);
// 用ArrayList对象来构造一个优先级队列的对象
// q3中已经包含了三个元素
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
System.out.println(q3.size());
System.out.println(q3.peek());
//自定义比较器
PriorityQueue<Integer> p4 = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
}
注:在默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户自定义比较器
class IntCmp implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
}
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
class Student implements Comparable<Student> {
public int age;
public Student(int age) {
this.age = age;
}
@Override
public int compareTo(Student o) {
return o.age - this.age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}
}
函数名 | 功能介绍 |
---|---|
boolean offer(E e) | 插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度0(logN) ,注意:空间不够时候会进行扩容 |
E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null |
E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
int size() | 获取有效元素的个数 |
void clear() | 清空队列 |
boolean isEmpty() | 检测优先级队列是否为空,空返回true |
JDK1.8 中PriorityQueue的扩容方式及其源码
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
扩容介绍:
- 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
- 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
- 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
堆排序即利用堆的思想来进行排序,总共分为两个步骤
/**
* 时间复杂度:0(N*logN)
* 空间复杂度:0(1)
* 稳定性:不稳定
* @param arr
*/
public static void heapSort(int[] arr) {
//建堆
createHeap(arr);
int end = arr.length - 1;
while (end >= 0) {
swap(arr, 0, end);
shiftDown(arr, 0, end);
end--;
}
}
/**
* 向下调整 -
* 时间复杂度 0(logN)
* @param arr 二叉树中存放的元素
* @param root 根节点的下标
* @param len 数组长度
*/
private static void shiftDown(int[] arr, int root, int len) {
int parent = root;
int child = (2 * parent) + 1;//左孩子
//孩子节点索引不会大于len
while (child < len) {
//使child索引位置保存子节点的最大值
if (child + 1< len && arr[child] < arr[child + 1]) {
child++;
}
if (arr[child] > arr[parent]) {
swap(arr, child, parent);
parent = child;
child = (2 * parent) + 1;
} else {
break;
}
}
}
/**
* 建堆
* 时间复杂度 0(n)
* @param arr
*/
private static void createHeap(int[] arr) {
for (int p = (arr.length - 1 - 1) / 2; p >=0; p--) {
shiftDown(arr, p, arr.length);
}
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大,所以对于一般排序是不可取的,最佳的方式是堆排序,基本思想如下:
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素