❤️值得收藏❤️优先级队列(堆)❤️全面讲解(易理解+附大量图+超详细)!!!
Posted 热爱编程的北北️
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❤️值得收藏❤️优先级队列(堆)❤️全面讲解(易理解+附大量图+超详细)!!!相关的知识,希望对你有一定的参考价值。
兄弟们,好久不见!!!相信你们一定想我了,但是,我更想你们。作为补偿,接下来要给大家带来更好更易懂的内容,一起交流一起进步鸭!老铁们可以互关哦!那么让我们出发吧!
优先级队列
什么是堆
-
优先级队列是一个类,即PriorityQueue,其底层最主要的是继承了AbstractQueue,而AbstractQueue又实现了Queue接口
-
优先级队列往往用堆来实现,堆,也即heap
-
堆逻辑上是一棵完全二叉树,物理上是保存在数组上,即其底层是一棵顺序存储的完全二叉树,一般只适合保存完全二叉树,因为非完全二叉树会有空间的浪费(中间不是连续的,自然会浪费空间)
补:完全二叉树:
完全二叉树(Complete Binary Tree)
- 若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
- 完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
- 一棵二叉树至多只有最下面的一层上的结点的度数可以小于2,并且最下层上的结点都集中在该层最左边的若干位置上,而在最后一层上,右边的若干结点缺失的二叉树,则此二叉树成为完全二叉树。
简单的来说,可以把完全二叉树理解为从上到下从左到右依次放节点,中间不能间断,如果每层都放满,则是满二叉树,因此,完全二叉树又是一种特殊的满二叉树
堆的基本概念与性质
基本概念
-
满足任意节点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆.
-
满足任意结点的值都小于其子树中结点的值,叫做小堆,或者小根堆,或者最小堆.
注意:不管是大根堆还是小根堆,左右孩子的大小关系是不确定的,我们只能确定根节点和孩子节点的关系
下标关系
- 已知双亲(parent)的下标,则:
左孩子(left)下标 = 2 * parent + 1;
右孩子(right)下标 = 2 * parent + 2; - 已知孩子(不区分左右)(child)下标,则:
双亲(parent)下标 = (child - 1) / 2;
作用
在了解了堆一般是分为大堆或者小堆后,我们就很容易地知道了如果在求最值时,即最大值,最小值,前K个最大,前K个最小,第K大,第K小等问题时可以用到堆
创建堆的方法
具体实现思路及代码
下面我们给出一个数组,这个数组逻辑上可以看做一棵完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?我们以创建大堆为例,调整方法为从最后一棵子树的根节点开始,向下调整,找左右孩子的最大值,然后最大值和根换,每一棵树都是这么调整的,这种方法叫做向下调整
例:
int[] array = { 27,15,19,18,28,34,65,49,25,37 };
此数组调整前用完全二叉树表示的形式是这样的,然后我们通过代码来和大家一块调整!
Heap部分
import java.util.Arrays;
public class Heap {
public int[] elem;
public int usedSize;
public Heap() {
this.elem = new int[10];
}
public void createHeap(int[] array) {
for (int i = 0; i < array.length; i++) {
this.elem[i] = array[i];
this.usedSize++;
}
//parent 就代表每颗子树的根节点
for(int parent = (array.length-1-1)/2;parent >= 0;parent--) {
//第2个参数 每次调整的结束位置应该是:this.usedSize.
adjustDown(parent,this.usedSize);
}
}
public void adjustDown(int root,int len) {
int parent = root;
int child = 2*parent+1;
while(child < len) {
//找到左右孩子的最大值
//1、前提是你得有右孩子
if(child+1 < len && this.elem[child] < this.elem[child+1]) {
child++;
}
//保证,child下标的数据 一定是左右孩子的最大值的下标
if(this.elem[child] > this.elem[parent]) {
int tmp = this.elem[child];
this.elem[child] = this.elem[parent];
this.elem[parent] = tmp;
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
}
测试类部分
import java.util.Arrays;
import java.util.PriorityQueue;
public class TestDemo {
public static void main(String[] args) {
int[] array = { 27,15,19,18,28,34,65,49,25,37 };
Heap heap = new Heap();
heap.createHeap(array);
System.out.println(Arrays.toString(heap.elem));
}
}
此处注意了,我们在测试类中创建的数组名是array,而实际调整的数组是elem,因此我们打印elem的结果即可看到我们调整为大堆的结果了:
实际效果图如下:
怎么样,是不是很简单呢?
时间复杂度分析(重难点)
时间复杂度通常指最坏的情况下处理问题所需的时间,在这里最坏的情况即为满二叉树的情况,这样每棵树都要进行调整
下面我们利用公式来计算:总的时间复杂度即为每层每个节点要调整的高度的和,即:
我们给等式两边同时乘以2,利用错位相减来求:
计算的结果为:
然后我们用等比数列的求和公式计算:
结果:
然后,我们需要对此式进行进一步地化简!
由于此树是一棵满二叉树,所以我们设一共有n个节点,此树的高度为h,那么可得公式:n=
2
h
2^h
2h-1,化简得h=log₂(n + 1),代入原式可得T(n) = n - log₂(n + 1)
由对数曲线可得,当n逐渐变大时,其结果逐渐趋于一个常数,所以可得时间复杂度为n
堆的应用
概念
在很多应用中,我们通常需要按照优先级情况对待处理对象进行处理,比如首先处理优先级最高的对象,然后处理次高的对象。最简单的一个例子就是,在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。
在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)
原理
优先级队列的实现方式有很多,但最常见的是使用堆来构建。
入队列
我们以大堆为例
- 首先按尾插方式放入数组
- 比较其和其双亲的值的大小,如果双亲的值大,则满足堆的性质,插入结束,此调整法为向上调整,即让要调整的节点与父亲节点比较
- 否则,交换其和双亲位置的值,重新进行 2、3 步骤
- 直到根结点
具体代码如下:
public void adjustUp(int child) {
int parent = (child-1)/2;
while (child > 0) {
if(this.elem[child] > this.elem[parent]) {
int tmp = this.elem[parent];
this.elem[parent] = this.elem[child];
this.elem[child] = tmp;
child = parent;
parent = (child-1)/2;
}else {
break;
}
}
}
public void push(int val) {
if(isFull()) {
//扩容
this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
}
this.elem[this.usedSize] = val;//10
this.usedSize++;//11
adjustUp(this.usedSize-1);//10下标
}
public boolean isFull() {
return this.usedSize == this.elem.length;
}
代码写完了,让我们测试下吧!
import java.util.Arrays;
public class TestDemo {
public static void main(String[] args) {
int[] array = { 27,15,19,18,28,34,65,49,25,37 };
Heap heap = new Heap();
heap.createHeap(array);
heap.push(55);
System.out.println(Arrays.toString(heap.elem));
}
}
输出结果
可以看出,我们成功建好了大堆并进行了扩容。
出队列
为了防止破坏堆的结构,删除时并不是直接将堆顶元素删除,而是用数组的最后一个元素替换堆顶元素,然后通过向下调整方式重新调整成堆
具体代码如下:
public void pop() {
if(isEmpty()) {
return;
}
int tmp = this.elem[0];
this.elem[0] = this.elem[this.usedSize-1];
this.elem[this.usedSize-1] = tmp;
this.usedSize--;//9 删除了
adjustDown(0,this.usedSize);
}
public boolean isEmpty() {
return this.usedSize == 0;
}
同样,让我们测试一下吧!
大功告成!
返回队首元素
这个很简单,只需直接返回即可:
public int peek() {
if(isEmpty()) {
throw new RuntimeException("队列为空");
}
return this.elem[0];
}
Java当中的优先级队列
- Java当中的优先级队列为PriorityQueue,底层是一个堆,默认容量是11
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable {
private static final long serialVersionUID = -7720805057305804111L;
private static final int DEFAULT_INITIAL_CAPACITY = 11;
transient Object[] queue; // non-private to simplify nested class access
实例化时默认调用无参的构造方法,而无参的构造方法又会调用带有两个参数的构造方法,即默认会调用带有两个参数的构造方法
- 那么,默认会建小堆还是大堆呢?让我们测试下:
PriorityQueue<Integer> priorityQueue=new PriorityQueue<>();
priorityQueue.offer(1);
priorityQueue.offer(53);
priorityQueue.offer(33);
System.out.println(priorityQueue);
打印结果为:
即:
可以看出默认会创建小堆。
- 错误处理:
错误处理 | 抛出异常 | 返回特殊值 |
---|---|---|
入队列 | add(e) | offer(e) |
出队列 | remove() | poll() |
队首元素 | element() | peek() |
TopK问题(重难点)
什么是TopK问题?简单的来说,就是在一个很大的数据里找到前K大或者前K小的数,这个问题也是十分经典的算法问题,不论是面试中还是实际开发中,都非常典型,下面我会以简单易理解的方式带大家学会
排序
关于此类问题,相信绝大部分人的第一反应就是用Arrays类自带的函数sort进行排序,然后取前K个,此方法不是不行,但是不容易在面试中脱颖而出,只有在实在没有其他办法的情况下,才可以先凑合着把这个方法写上
时间复杂度:O(n*lg(n))
排序的优化
该方法与第一个排序方法类似,用一个容器保存前K个数,然后将剩余的所有数字——与容器内的最小数字相比,若所有后续的元素都比容器内的K个数还小,那么容器内的K个数就是最大K个数。如果某一后续元素比容器内最小数字大,就删掉容器内最小元素,并将该元素插入容器,最后遍历完所有的数,得到的结果容器中保存的数即为最终结果了。
时间复杂度:O(n+m^2)(m为K的大小)
利用堆
此方法利用堆的特性来解,思路为维护一个大小为 K 的小顶堆,依次将数据放入堆中,当堆的满了的时候,将堆顶元素与下一个数比较:如果大于堆顶元素,则将当前的堆顶元素抛弃,并将该元素插入堆中。遍历完全部数据,要求的前K个最大的元素也自然都在堆里面了。
首先将数据插入堆,维持一个小顶堆
因为85大于堆顶元素,所以进行替换
继续维持小顶堆
注意
- 若要求前K个最小,只需变成大顶堆即可
- 遍历完全部数据后,堆顶元素即为第K大的数据
时间复杂度:O(nlogK)
实现代码如下:
public static void topk(int[] array,int k) {
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
//大堆
for (int i = 0; i < array.length; i++) {
if(maxHeap.size() < k) {
maxHeap.offer(array[i]);
}else {
int top = maxHeap.peek();
if(top > array[i]) {
maxHeap.poll();
maxHeap.offer(array[i]);
}
}
}
System.out.println(maxHeap);
}
接下来我们来测试一下吧!假如我们要求前3个最小元素:
int[] array = {1,25,2,10,5,35,21,19,56};
topk(array,3);
结果如下:
成功了!!!是不是很简单?相信你只要认真多看看肯定会明白的呢
堆排序(重难点)
- 堆排序是利用堆这种数据结构设计出的一种排序算法,其是选择排序的一种,它利用大顶堆(小顶堆)堆顶元素是最大值(最小值)这一特性,使得每次从无序中选择最大值(最小值)变得简单。
- 排升序要建大堆;排降序要建小堆。
具体步骤如下
step1:先将带排序的数组构造成一个大根堆,假设有如下数组:int[] array2={2,3,4,1,6,5};
构造成大根堆如下:
step2:将堆顶元素与堆尾元素交换:
step3:将除6以外其他的所有元素继续构造大根堆:
以此类推,然后再将堆顶元素与堆中倒数第二个元素交换,换完之后除了倒数第一个和倒数第二个元素以外,其他元素继续构造成大堆,最终会得到有序的数组
同理,如果要从大到小排,则构建小堆即可!
public static void siftDown(int[] array,int root,int len) {
int parent = root;
int child = 2*parent+1;
while (child < len) {
//找到左右孩子的最大值
//1、前提是你得有右孩子
if(child+1 < len && array[child] < array[child+1]) {
child++;
}
//child的下标就是左右孩子的最大值下标
if(array[child] > array[parent]) {
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
public static void createHeap(int[] array) {
//从小到大排序 -》 大根堆
for (int i = (array.length-1 - 1) / 2; i 以上是关于❤️值得收藏❤️优先级队列(堆)❤️全面讲解(易理解+附大量图+超详细)!!!的主要内容,如果未能解决你的问题,请参考以下文章