堆和优先队列
什么是优先队列
- 普通队列:先进先出;后进后出
- 优先队列:出对顺序和入队顺序无关;和优先级有关
优先队列的实现
- 通常情况下,我们可以使用两种方法实现优先队列。
- 1、我们直接采用普通线性结构,诸如数组或者链表。这样的话,入队操作变得非常简单,只是O(1)的复杂度,但是出队操作就是O(n)的复杂度,优先级最高我们可以理解为取出最大/最小元素。
- 2、我们采用顺序线性结构,这里的顺序线性结构是指我们所有的元素维持着一种顺序。这样我们如果需要取出最大/最小元素,只需要O(1)的时间复杂度,但是我们如果要入队一个元素的话,则需要找一个合适的位置插入,即入队操作的复杂度为O(n)。
- 3、我们在这里使用堆来实现优先队列,这时候不管是入队操作还是出队都是都是O(logn)的时间复杂度,并且是在最坏情况下都是O(logn),这表明堆是一种非常高效的数据结构。
堆的基本结构
其实一个堆也是一个二叉树,接下来我们主要是实现一个二叉堆。
二叉堆是一颗完全二叉树,二叉堆中一个非常重要的性质就是:堆中某个节点的值总是不大于其父节点的值。
即二叉堆的根节点的元素是最大的,这就非常方便了。以这种方式定义的堆叫做“最大堆”。
同理,我们也可以定义出来最小堆,即根元素的节点是最小值。
如上图所示,我们这样定义一个二叉堆。即我们可以用一个数组来存储二叉堆。那么接下来问题的关键就是,我们如何来找到这些索引呢,通过上面的图,我们不难发现,某节点的左孩子的索引就是该节点索引的两倍,该节点的右孩子就是该节点索引的两倍+1.
用数组来定义一个二叉堆的好处还不止上面这些,我们要想得到某个节点的父亲节点,同样也很方便,直接为该节点的索引值/2.
以上方式我们是把初始索引定义成1来计算,但是为了计算机语言的方便,我们还是将初始位置定义为0,即我们只需要做一点点的偏移即可。
如上图所示,上述我们可以通过数学归纳法来证明,在这里就不详细展开了。就这样我们就实现了一个基于数组实现的二叉堆!
在这里,首先我引入自己以前定义的一个动态数组实现,由于比较简单,在这里就不详细说明了。
1 | public class Array<E> { |
1 | public class MaxHeap<E extends Comparable<E>> { |
相信,有了上面对最大堆的分析,下面我们要实现一个以最大堆为基础的优先队列就很容易了!
- 首先我们定义出队列的接口
1 | public interface Queue<E> { |
- 然后定义优先队列
1 | public class PriorityQueue<E extends Comparable<E>> implements Queue<E> { |
就这样,我们就很简单的实现了一个最大堆。
优先队列的经典问题
接下来,我们将引入一个优先队列的经典问题。
在1000000个元素中取出前100名,更加常见的情形就是在N个元素中选出前M个元素。
我们最容易想到的方法就是对这1000000个元素先排序,我们采用高级的排序算法,例如归并排序或者快速排序,都可以达到NlogN的复杂度。
但是,我们如果用优先队列的话,可以达到NlogM的时间复杂度,一般情况下M是远小于N的。这样就帮助我们大大改善了性能。
以Leetcode 347、前K个高频元素为例
https://leetcode-cn.com/problems/top-k-frequent-elements/
1 | class Solution { |
- 这道题的关键就是我们如何理解优先级和频次的关系。
- 我们利用Map映射存储元素,和他的频次。
- 在这里,我们定义为频次低,为优先级高,这就方便了我们取出频次低的元素,并且将频次高的元素入队,这样,最后剩下的元素,都是频次高的,即我们可以取出前k个频次高的元素。