目录
1、优先级队列
1.1 概念
1.2 PriorityQueue底层结构
2、 堆
2.1 堆的概念
2.2 堆的存储结构
3、优先级队列(堆)的模拟实现
3.1 堆的创建
3.1.1 向下调整算法建完整堆
3.2 堆的插入
3.2.1 向上调整算法
3.3 堆的删除
3.4 堆排序
4、PriorityQueue集合
4.1 PriorityQueue的特点
4.2 PriorityQueue的方法
4.3 PriorityQueue源码分析
4.3.1 成员变量
4.3.2 构造方法
4.3.3 插入方法(offer)
4.3.4 扩容方法(扩容机制)
4.4 TOP-K问题
4.4.1 TOP-K问题 力扣OJ题
1、优先级队列
1.1 概念
对于队列而言,数据只能从队尾进,队头出,遵循着固定的先进先出原则。
而在某些特殊场景需求下,要求优先级高的元素先出队列,
在这种情况下,数据结构应该提供两个最基本的操作:
- ①:不仅能添加新对象
- ②:还能返回最高优先级对象。
这种数据结构就是优先级队列,Java也提供了PriorityQueue集合。
1.2 PriorityQueue底层结构
PriorityQueue的底层是堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整,接下来,让我们来聊一聊堆到底是什么。
2、 堆
2.1 堆的概念
所有元素按完全二叉树的顺序存储方式存储在一个一维数组中。
堆分为 大根堆与小根堆。
大根堆:每个节点都大于或等于其子节点。
小根堆:每个节点都小于或等于其子节点。
2.2 堆的存储结构
我们已知堆为一棵完全二叉树,故采用顺序的方式存储在数组中。
而对于我们之前所学的二叉树,为什么没有采用顺序存储而是采用链式存储呢?
因为二叉树并非都为完全二叉树,若采用顺序存储会造成空间浪费:
因为堆采用顺序的方式进行存储,且为完全二叉树,故具有以下性质:
- 孩子节点下标为i,则其父亲节点下标为(i - 1)/2
- 父亲下标为i,则其左孩子下标为2i+1(2i+1<节点总数的情况下,否则无左孩子)
- 父亲下标为i,则其右孩子下标为2i+2(2i+2<节点总数的情况下,否则无右孩子)
3、优先级队列(堆)的模拟实现
注:下文中代码实现的为大根堆
3.1 堆的创建
向下调整算法
向下调整算法:选出其左右孩子中值较小值元素(建大堆就选较大元素,建小堆就选较小元素,这里以建小堆为例),将这个元素和根节点进行比较,若比根节点还小,就和根节点交换,交换后可能导致子树不满足堆的性质,因此需要继续向下调整。
注意:向下调整算法的使用,必须要求其左右子树必须为大根堆或者小根堆!!!
向下调整算法的时间复杂度为:O(logN),因为最坏情况是从根一路比较到叶子,比较的次数即为完全二叉树的高度次。
3.1.1 向下调整算法建完整堆
我们给出一组数据:{ 27,15,19,18,28,34,65,49,25,37 },要想将这组数据建成堆,我们可以使用向下调整法建堆。
而一组乱序数据其左右子树是不为堆的,那就需要通过向下调整算法从倒数第一个非叶子节点(下标为(数组.length-1-1)/2 )开始建堆,直到将整棵树建成堆。
建堆的时间复杂度为:O(N)!!!,
为什么不是O(N*logN)呢?这里给出解释:
向下调整整体建堆代码:
/**
* 建堆整体时间复杂度:O(N)
*/
public void createHeap() {
//从倒数第一个飞非叶子节点开始建堆
int parent = (this.usedSize - 1 - 1)/2;
while (parent >= 0) {//O(N)
siftDown(parent,this.usedSize);
parent--;
}
}
/**
*向下调整算法
* 时间复杂度:O(logN)
* @param parent 向下调整的起始位置,即根节点
* @param usedSize 标定调整的结束 若算出的孩子节点坐标>=usedSize时,说明已调整完成
*/
private void siftDown(int parent, int usedSize) {
int child = parent*2+1;
//当没有孩子节点时,说明向下调整完成
while (child < usedSize) {
//当右孩子存在时,选出左右孩子的最大值(建大堆)
if (child + 1 < usedSize) {
if (elem[child] < elem[child + 1]) {
child = child + 1;
}
}
//和根节点进行比较
if (elem[child] > elem[parent]) {
swap(elem, parent, child);
parent = child;
child = parent*2+1;
}else {
//若根节点大,说明已是大堆,break结束
break;
}
}
}
3.2 堆的插入
插入数据总共有两个步骤:
- 将新元素插入堆的末尾(插入后不再为堆)
- 使用向上调整算法调整为堆
3.2.1 向上调整算法
(这里以建小堆为例):将元素插入到堆的末尾后,和根节点进行比较,如果比根节点还小就和根节点换,继续向上调整,直至满足堆的性质。如果没有根节点小,说明此时已满足堆的性质,调整完成。
时间复杂度:O(logN)
插入元素代码:
/**
* 插入元素
* 时间复杂度:O(logN)
* @param x:新插入元素的值
*/
public void offer(int x) {
if (isFull()) {
grow();
}
elem[usedSize] = x;
siftUp(usedSize);
usedSize++;
}
/**
* 向下调整算法
* @param childIndex:新插入元素的下标
*/
public void siftUp(int childIndex) {
//找到新节点的父节点的下标
int parent = (childIndex - 1)/2;
//parent为负数时,说明调整完成(最坏)
while (parent >= 0) {
if (elem[parent] < elem[childIndex]) {
swap(elem,parent,childIndex);
childIndex = parent;
parent = (childIndex - 1)/2;
}else {
break;
}
}
}
/**
* 如果堆底层的数组满了,就扩容
*/
private void grow() {
this.elem = Arrays.copyOf(elem,elem.length*2);
}
向上调整算法建堆【拓展】
故我们也可以一个一个的插入元素(使用向上调整算法)来建堆,但是不推荐,因为时间复杂度比向下调整算法建堆要大,为:O(N*logN)
其实主要原因就是:最后一层的元素是最多的,都要一个个向上调整
3.3 堆的删除
堆的删除一定删除的是堆顶元素。
故,删除元素的思想很简单,即:
- 将堆顶元素和堆中最后一个元素交换
- 将堆中有效数据个数(usedSize)减一
- 对堆顶元素进行向下调整(只需要调整0下标就可以了)
堆删除代码:
/**
* 删除堆元素
*/
public void poll() {
if (isEmpty()) {
//如果堆为空,返回
return;
}
//交换堆顶和最后一个元素
swap(elem,0,usedSize-1);
//堆元素有效个数-1
usedSize--;
//只向下调整0下标即可
siftDown(0,usedSize);
}
3.4 堆排序
若要升序排列,要建大根堆;若要降序排列,就要建小根堆。
以排升序为例:若要排升序,则为大堆,排序过程如下:
- 将堆顶元素和堆末元素交换,有效数据个数减一(因为堆顶元素为最大值元素,此时最大值元素已来到数组末尾)
- 将0下标处向下调整,重新调整为大堆
- 继续将堆顶元素和堆末元素交换,有效数据个数减一(堆顶元素为次大值元素,此时次大值元素已来到数组末尾倒数二个位置处)
- 将0下标处向下调整,重新调整为大堆
- 重复以上过程,直至只剩一个元素的时候,此时数组已有序且为升序排列
堆排序 排升序代码 :
/**
* 堆排序
*/
public void heapSort() {
if (isEmpty()) {
return;
}
int end = usedSize-1;
while (end > 0) {
swap(elem,0,end);
siftDown(0,end);
end--;
}
}
4、PriorityQueue集合
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线 程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
4.1 PriorityQueue的特点
- 我们知道,堆的建立和每次的插入删除数据都需要需要对堆进行调整,意味着要有可比较大小的功能。PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象(自定义类要实现Comparable接口或者提供比较器),否则会抛出ClassCastException异常。
- 不能插入null对象,否则会抛出NullPointerException
- 当元素满时,会自动扩容
- 插入和删除元素的时间复杂度为O(logN)(删除要向下调整,插入要向上调整,但最多调整高度次)
- PriorityQueue底层使用了堆数据结构
- PriorityQueue默认情况下是小堆---即每次获取的堆顶元素都是最小的元素
4.2 PriorityQueue的方法
4.3 PriorityQueue源码分析
4.3.1 成员变量
PriorityQueue的底层是堆,使用的是顺序结构来存储数据。
4.3.2 构造方法
以上的三个构造方法,实际上调用的就是带两个参数的构造方法:
- 第一个参数是容量的大小,若没有指定容量,那默认容量就是11
- 第二个参数是比较器,用户可以自定义比较器:直接实现Comparator接口,然后重写该接口中的compare方法,给出比较的功能,若没有提供那就是null
当然,我们也可以用集合传参来构造优先级队列:
4.3.3 插入方法(offer)
我们先来分析源码:
通过观察offer方法(插入数据)的底层源码,我们得出结论:
- ①:向上调整时,如果我们提供了比较器,就通过比较器比较;如果没有提供比较器,就通过Comparable接口的方法进行比较(比较器和Comparable两者必有其一)
- ②:建堆时默认建的是小堆,如果要建大堆,我们可以修改比较器或者Comparable接口中compareTo方法的内部实现
上图所分析的是使用Comparable中的方法进行比较,若使用比较器来进行比较的话,内部思想其实是一模一样的。
4.3.4 扩容方法(扩容机制)
- 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
- 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
- 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容(最多就是MAX_ARRAY_SIZE)
4.4 TOP-K问题
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
目前解决这个问题我们有三种思路,设共有N个数据:
- 将全部N个元素排序,取前K个元素
- 将整体N个元素建成堆,拿出堆顶元素,调整;再拿出堆顶元素,再调整......依次拿出K个元素
- (求前K个最小元素):把前K个元素建成大根堆,将堆顶元素依次和剩下的N-K个元素比较,若比堆顶元素小,就删除堆顶元素并将这个元素入堆,将剩余的元素全部比较完后,堆中的K个元素就是前K个最小元素,堆顶元素就是第K个最小元素。(建大根堆的原因就是,堆顶元素是堆中最大的,和其余N-K个元素比较后,堆中元素就是所有元素中最小的,而堆顶元素就是第K小元素)
我们分析以上三种思路的时间复杂度:
思路一:最快的排序算法也只能为:O(N*logN)
思路二:使用集合时,我们只能使用offer方法插入元素向上调整来建堆, 为O(N*logN),接着依次出K个元素,每次出完都要调整,为O(K*logN),故整体为O((N+K)*logN)
思路三:前K个元素建堆的时间复杂度为:O(K*logK),剩余N-K个元素依次比较和调整为堆的时间复杂度为:O((N-K)*logK),故整体为:O(N*logK)
我们可以看出思路三的时间效率是最高的,所以我们可以使用思路三来解决遇见的TOP-K问题。
思路三总结:
- 求前K个最大元素,建小堆。
- 求前K个最小元素,建大堆。
4.4.1 TOP-K问题 力扣OJ题
. - 力扣(LeetCode)
代码:
/**
* 最小前K个数 ->建大堆
*/
class Solution {
public int[] smallestK(int[] arr, int k) {
PriorityQueue<Integer> priorityQueue =
new PriorityQueue<>(new Comparator<Integer>() {
//匿名内部类 建大堆-》修改比较器方法
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
int[] ret = new int[k];
if(arr == null || k == 0) {
return ret;
}
for (int i = 0; i < k; i++) {
priorityQueue.offer(arr[i]);
}
for (int i = k; i < arr.length; i++) {
if (arr[i] < priorityQueue.peek()) {
priorityQueue.poll();
priorityQueue.offer(arr[i]);
}
}
for (int i = 0; i < k; i++) {
ret[i] = priorityQueue.poll();
}
return ret;
}
}
OK~本次博客到这里就结束了,
感谢大家的阅读~欢迎大家在评论区交流问题~
如果博客出现错误可以提在评论区~
创作不易,请大家多多支持~