目录
1,优先级队列
1.1 概念
2,优先级队列的模拟实现
2.1 堆的概念
2.2 堆的存储方式
2.3 堆的创建
2.3.1 堆的向下调整(大根堆)
2.3.2 建堆的时间复杂度编辑
2.4 堆的插入与删除
2.4.1 堆的插入
2.4.2 堆的删除
3,常用接口介绍
3.1 PriorityQueue的特性
3.2 PriorityQueue常用接口介绍
3.2.1 优先级队列的构造
3.2.2 插入/删除/获取优先级最高的元素
3.2.3 PriorityQueue的大根堆,小根堆转换
3.2.4 PriorityQueue的扩容方式
3.3 oj练习
3.3.1 top-k问题
3.3.2 求一组数据第k小的元素
3.3.3 求一组数据第k大的元素
4,堆的应用
4.1 堆排序
1,优先级队列
1.1 概念
前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。
在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
2,优先级队列的模拟实现
JDK1.8中的PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。
2.1 堆的概念
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一 个一维数组中,并满足:Ki = K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值
堆总是一棵完全二叉树。
2.2 堆的存储方式
从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储。
上图,对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
2.3 堆的创建
2.3.1 堆的向下调整(大根堆)
因为是以数组存储的,先创建好数组。
此时数组elem中存储的数据为:{27,15,19,18,28,34,65,49,25,37}
从二叉树的角度来看,是这样的:
调整成大根堆:
注意:在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆了才可以向下调整。 时间复杂度分析:
最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(log以2为底的N)
2.3.2 建堆的时间复杂度
因此:建堆的时间复杂度为O(N)。
2.4 堆的插入与删除
2.4.1 堆的插入
上面已经建立好大根堆了,现在要对他进行插入和删除
比如:我们插入一个80,插入完成以后也要保证他是一个大根堆
因为我们操作的是数组,首先得判断是否已满,满了进行扩容
所以我们就可以通过offer元素来创建大根堆,这是向上调整
创建大根堆的方式可以在一个数组已经给定的情况下,在数组内进行调整(向下调整)
也可以一个一个元素offer进行调整成大根堆(向上调整)
所以向上调整的时间复杂度是很高的
2.4.2 堆的删除
如果说现在是一个大根堆,要删除一个元素,删除的是堆顶元素(优先级队列,优先拿到的堆顶元素),删除一个元素之后,也要保证他还是一个大根堆
删除逻辑:
习题:
3,常用接口介绍
3.1 PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
关于PriorityQueue的使用要注意:
1. 使用时必须导入PriorityQueue所在的包,即:
2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常
假如有一个学生类: 没有说明两个Student根据什么比较,所以之前讲过自定义类型,要实现Comperable接口
3. 不能插入null对象,否则会抛出NullPointerException
4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
但是没有容量限制,不是说无线可以往里面放,堆是有大小的
5. 插入和删除元素的时间复杂度为O(log以2为底的N),因为插入以后是向上调整,最多调整的是树的高度,删除也是,第一个和最后一个换,最坏情况0下标一直向下调整,也是树的高度
6. PriorityQueue底层使用了堆数据结构
7. PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素
3.2 PriorityQueue常用接口介绍
3.2.1 优先级队列的构造
那么不能是大根堆吗? 答:可以,后面会讲到
此时呢,我们调用的是不带参数的构造方法,进入源码
源码:
代表只要实现Collection接口的,也就是说Collection接口能引用的对象都能接收
3.2.2 插入/删除/获取优先级最高的元素
3.2.3 PriorityQueue的大根堆,小根堆转换
首先进入offer源码:
上面讲过:我们通过不带参数的构造方法,会给queue分配一个长度大小为11的数组
此时数组长度为11,那么我们第一次放一个数据为10
第二次offer,比如是5
对于上图的CompareTo,只要你插入的元素没有你原来的元素大,就会发生交换!!!
所以和CompareTo的实现有关系
换句话说,如果把if里面的比较改成<=0,那就不一样了
可以看到,这是一个大根堆。
所以,CompareTo实现的方式不一样,就可以控制他是大根堆还是小根堆
现在,我们放的是Integer
进入他的源码:他是实现了Comparable接口的,compareTo自己实现了compare方法
那么回到siftUpComparable
PriorityQueue默认是小根堆,就是因为在调用compareTo的时候比较方式来决定的。
那如果说变成大根堆,我们就可以在自己传入一个比较器
如果说传comparator,只需要把if里面的比较改成<=0,换句话说现在把下面代码变成大根堆
就要自己去传一个比较器
当我们自己实现一个比较器的时候,siftDown方法会走if语句
在这种情况下就是小根堆。
当我们改一下比较器的传值
所以,我们就可以把优先级队列看作是小根堆,也可以看作是大根堆,去控制compare的返回值
3.2.4 PriorityQueue的扩容方式
进入offer源码:
当 i >= 11了,数组长度是11,也就是数组满了,满了以后进行扩容
那我们来看一下这里的扩容,进入grow的源码:
在JDK1.8中,申请数组的最大长度:Integer.MAX_VALUE(21亿多)- 8
优先级队列的扩容说明:
如果容量小于64时,是按照oldCapacity的2倍方式扩容的
如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
3.3 oj练习
3.3.1 top-k问题
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
此时并没有什么问题,代码可以通过。但是不是非常好的解决方案!!!
时间复杂度:
所以这不是真正topK的真正解法!!!
那么如何解决,怎样的解决方式才能实现很小的时间复杂度?
面试经常问到!!!重点!!!是堆的一个应用!!!
做法:
在这种题里面,往往k是一个很小的数,理论上认为他的时间复杂度为:O(N)
3.3.2 求一组数据第k小的元素
3.3.3 求一组数据第k大的元素
4,堆的应用
4.1 堆排序
建堆的时间复杂度:O(N),排序的时间复杂度:O(n*logN)
习题: