文章目录
- 1.优先级队列
-
- 1.1概念
- 2.堆的模拟实现
-
- 2.1 堆的概念
- 2.2 堆的存储方式
- 2.3 堆的创建
-
- 2.3.1 堆的向下调整(shiftDown)
- 2.3.2 堆的创建
- 2.3.3 建堆的时间复杂度
- 2.4堆的插入和删除
-
- 2.4.1 堆的插入
- 2.4.3 堆的删除
- 3.Java中的优先级队列
-
- 3.1 PriorityQueue
- 3.2 PriorityQueue常用接口介绍
-
- 1. 构造方法
- 2.常用方法
- 3. 扩容方式
- 4. 堆的应用
-
- 4.1 堆排序
- 4.2 Top-k问题
1.优先级队列 1.1概念 之前我们学习过队列是一种先进先出的数据结构,但在某些场景下,操作的数据具有优先级,在出队列时,需要优先级高的数据先出队列。比如在打游戏时来电话了,那么操作系统应该先处理电话;还有在游戏英雄联盟中泰坦的Q的优先级比机器人高;
诸如这些情况下,我们需要数据结构提供两种最基本的操作,首先添加新的对象,然后返回最高优先级对象,这种数据结构就是优先级队列(Priority Queue).
2.堆的模拟实现 在JDK1.8中的PriorityQueue底层使用了堆的数据结构,而堆通常是一个可以被看做一棵完全二叉树的数组对象。
2.1 堆的概念 如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆
堆满足以下性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值。
- 堆总是一棵完全二叉树。
文章图片
文章图片
注:对应非完全二叉树则不适用于顺序方式来存储,因为为了通过顺序表来还原二叉树,则空间中就需要存储空结点,这样会导致空间利用率低。
当元素存储到数组中后,我们如何还原二叉树呢?假设i为节点在数组中的下标,
- 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
- 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
- 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
parent = i / 2;
leftChild = 2 * i;
rightChild = 2 * i + 1;
//或者已知父节点为root
parent = root;
leftChild = (2 * parent) + 1;
rightChild = (2 * parent)+ 1;
2.3 堆的创建 2.3.1 堆的向下调整(shiftDown)
对于集合{ 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;
}
2.3.2 堆的创建
对于一个普通序列,即根的左右子树都不满足堆的特性,那么该如何调整呢?
文章图片
/**
* 建堆
* 时间复杂度 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);
}}
2.3.3 建堆的时间复杂度
堆是完全二叉树,这里我们简化用满二叉树来证明2.4堆的插入和删除 2.4.1 堆的插入
文章图片
所以,建堆的时间复杂度为0(N)
插入步骤:
- 先把元素放到堆最后(即数组最后),如果空间不足,则需要扩容。
- 将插入的元素向上调整,直到满足堆的性质
文章图片
/**
* 向堆中插入元素
* @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;
}/**
* 向上调整 - 向堆中添加元素 - 只能添加到末尾然后向上调整
*/
privatevoid 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;
}
}
}
2.4.3 堆的删除
删除步骤:
注:堆的删除一定是删除优先级高的元素(即堆顶元素)
- 将堆顶元素和堆中最后的元素交换
- 有效数据减一
- 从堆顶元素开始,向下调整
文章图片
/**
* 出堆顶元素
*/
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;
}
}
}
3.Java中的优先级队列 3.1 PriorityQueue Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,我们主要介绍PriorityQueue。
使用注意事项:3.2 PriorityQueue常用接口介绍 1. 构造方法
- 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 q1 = new PriorityQueue<>();
// 创建一个空的优先级队列,底层的容量为initialCapacity
PriorityQueue q2 = new PriorityQueue<>(100);
ArrayList list = new ArrayList<>();
list.add(4);
list.add(3);
list.add(2);
list.add(1);
// 用ArrayList对象来构造一个优先级队列的对象// q3中已经包含了三个元素
PriorityQueue q3 = new PriorityQueue<>(list);
System.out.println(q3.size());
System.out.println(q3.peek());
//自定义比较器
PriorityQueue p4 = new PriorityQueue<>(new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
}
注:在默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户自定义比较器
- 自定义比较器
class IntCmp implements Comparator {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
}
- 使用匿名内部类
PriorityQueue priorityQueue = new PriorityQueue<>(new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
- 自定义类中实现Comparable接口
class Student implements Comparable {
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 +
'}';
}
}
2.常用方法
函数名 | 功能介绍 |
---|---|
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;
}
【数据结构|【数据结构】优先级队列 - 堆】扩容介绍:4. 堆的应用 4.1 堆排序 堆排序即利用堆的思想来进行排序,总共分为两个步骤
- 如果容量小于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;
}
4.2 Top-k问题 TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大,所以对于一般排序是不可取的,最佳的方式是堆排序,基本思想如下:
- 用数据集合中前K个元素来建堆
1.1 前k个最大的元素,则建小堆
1.2 前k个最小的元素,则建大堆 - 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
推荐阅读
- JavaSE|【Java小游戏】俄罗斯方块
- 数据结构于算法|十大排序算法基本原理及其实现
- JavaSE|【JavaSE】数组
- 即时通讯源码对企业到底有多重要呢()
- 牛客刷题集锦|『牛客|每日一题』 栈的压入、弹出序列
- 牛客刷题集锦|『牛客|每日一题』模板栈
- 后端|分布式锁用 Redis 还是 Zookeeper(看完你就明白了)
- #|红黑树的学习
- 笔记|SpringBoot发送邮件(详细学习笔记)