目录
[2.1 堆的概念](#2.1 堆的概念)
[2.2 堆的存储方式](#2.2 堆的存储方式)
[2.3 堆的创建](#2.3 堆的创建)
[2.4 堆的插入和删除](#2.4 堆的插入和删除)
[2.5 用堆模拟实现优先级队列](#2.5 用堆模拟实现优先级队列)
[3.1 PriorityQueue的特性](#3.1 PriorityQueue的特性)
[3.2 PriorityQueue常用接口](#3.2 PriorityQueue常用接口)
[3.3 练习](#3.3 练习)
[4.1 PriorityQueue的实现](#4.1 PriorityQueue的实现)
[4.2 堆排序](#4.2 堆排序)
[4.3 Top-k问题](#4.3 Top-k问题)
一、优先级队列
队列是一种先进先出的数据结构 ,但有些情况下,操作的数据可能带有优先级,一般出队
列时,可能需要优先级高的元素先出队列,该场景下,使用队列显然不合适。在这种情况下,数据
结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数
据结构就是 优先级队列 (Priority Queue) 。
二、优先级队列的模拟实现
JDK1.8中的优先级队列****底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进
行了一些调整。

2.1 堆的概念
如果有一个关键码的集合K = {k0,k1,k2,...,****kn-1},把它的所有元素按完全二叉树的顺
序存储方式存储在一****个一维数组中,并满足:Ki <= K2i+1且Ki<= K2i+2 (Ki >= K2i+1 且 Ki >=
K2i+2) i = 0,1,2...,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆 ,根节点最
小的堆叫做最小堆或小根堆。

性质:堆中某个节点的值总是不大于或不小于其父节点的值;堆总是一棵完全二叉树。
2.2 堆的存储方式
堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储。
注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
将元素存储到数组中后,可以根据二叉树的性质5对树进行还原。假设i为节点在数组中的下标,则有
- 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
- 如果 2 * i + 1 小于节点个数,则节点 i 的左孩子下标为 2 * i + 1 ,否则没有左孩子
- 如果 2 * i + 2 小于节点个数,则节点 i 的右孩子下标为 2 * i + 2 ,否则没有右孩子
2.3 堆的创建
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,将其创建成堆

堆向下调整过程(以大堆为例):
- 让parent标记需要调整的节点的下标,child标记parent的左孩子下标(parent如果有孩子一定先有左孩子 )
- 如果parent左孩子存在,即:child < len, 进行以下操作,直到parent左孩子不存在(len为节点个数)
- 判断parent右孩子是否存在,如果存在找到左右孩子中最大的孩子,child存储最大的孩子下标
- 将parent与较大的孩子child比较,如果parent大于较大的孩子child,调整结束;否则:交换parent与较大的孩子child,交换完成后,parent中大的元素向下移动,可能导致子树不满足堆的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续进行2
private void siftDown(int parent,int len){ //len是堆中有效数据的个数 //child记录孩子的下标,因为有可能有孩子也可能没有孩子 int child = 2*parent+1; //至少有左孩子 while(child < len){ //有右孩子 且 左右孩子比较 if(child+1 < len && elem[child] < elem[child+1]){ child = child+1; } //执行完if语句,child是左右孩子最大的下标 if(elem[child] > elem[parent]){ swap(child,parent); //判断子树-进行循环 parent = child; child = 2*parent+1; }else { break; } } }
//交换 private void swap(int i, int j){ int tmp = elem[i]; elem[i] = elem[j]; elem[j] = tmp; }
创建一个堆,从最后一棵子树开始向下调整,调整完成后调整前一棵子树,直至所有子树全部调整完成 。找最后一棵子树根节点下标 i:i = (len - 1)/2,len是二叉树的结点个数。
创建大根堆:


最终效果图:

时间复杂度 :堆是完全二叉树,而满二叉树也是完全二叉树,为简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响结果)

故建堆的时间复杂度为O(N)。
2.4 堆的插入和删除
1.堆的插入
堆的插入总共有两个步骤:
- 先将元素放入到底层空间中(注意:空间不够时需要扩容)
- 将最后新插入的节点向上调整,直到满足堆的性质
public void siftUp(int child){ //获取child的双亲 int parent = (child-1)/2; while (child > 0){ //双亲parent比孩子child大,满足堆的性质,调整结束 if(elem[parent] > elem[child]){ break; }else { swap(parent,child); //大的元素向上移动,可能子树不满足堆的性质,需要继续向上调整 child = parent; parent = (child-1)/2; } } }
//交换 private void swap(int i, int j){ int tmp = elem[i]; elem[i] = elem[j]; elem[j] = tmp; }
2.堆的删除
**堆的删除一定删除的是堆顶元素。**具体如下:
- 将堆顶元素对堆中最后一个元素交换
- 将堆中有效数据个数减少一个
- 对堆顶元素进行向下调整
public int pop(){ //堆为空 if(isEmpty()){ return -1; } //获取出堆元素 int ret = elem[0]; //出堆的元素和最后一个元素交换 swap(0,usedSize-1); usedSize--; //向下调整 siftDown(0,usedSize); return ret; }
2.5 用堆模拟实现优先级队列
//大根堆 public class MyPriorityQueue { private int[] elem; //堆中有效元素个数 private int usedSize; public MyPriorityQueue(){ elem = new int[10]; } //判断堆是否满 private boolean isFull(){ return usedSize == elem.length; } //判断堆是否为空 public boolean isEmpty(){ return usedSize == 0; } /** * 初始化elem数组 */ public void initElem(int[] array){ for(int i = 0; i < array.length; i++){ elem[i] = array[i]; usedSize++; } } /** * 使用向下调整创建大根堆 */ public void createHeap(){ for(int i = (usedSize-1-1)/2;i >= 0; i--){ siftDown(i,usedSize); } } //向下调整 private void siftDown(int parent,int len){ int child = 2*parent+1; //至少有左孩子 while(child < len){ //有右孩子 且 左右孩子比较 if(child+1 < len && elem[child] < elem[child+1]){ child = child+1; } //执行完if语句,child是左右孩子最大值的下标 if(elem[child] > elem[parent]){ swap(child,parent); parent = child; child = 2*parent+1; }else { break; } } } //交换 private void swap(int i, int j){ int tmp = elem[i]; elem[i] = elem[j]; elem[j] = tmp; } //入堆 public void push(int val){ //堆满 if(isFull()){ elem = Arrays.copyOf(elem,elem.length*2); } elem[usedSize] = val; //向上调整 siftUp(usedSize); usedSize++; } //向上调整 public void siftUp(int child){ int parent = (child-1)/2; while (child >0){ if(elem[parent] > elem[child]){ break; }else { swap(parent,child); child = parent; parent = (child-1)/2; } } } //出堆 public int pop(){ //堆为空 if(isEmpty()){ return -1; } //获取出堆元素 int ret = elem[0]; //出堆的元素和最后一个元素交换 swap(0,usedSize-1); usedSize--; //向下调整 siftDown(0,usedSize); return ret; } }
三、常用接口
3.1 PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的。

使用PriorityQueue需要注意:
- 使用时必须导入PriorityQueue所在的包
import java.util.PriorityQueue;
-
PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
-
不能插入null对象,否则会抛出****NullPointerException
4.没有容量限制,可以插入任意多个元素,内部可以自动扩容
-
插入和删除元素的时间复杂度为O(
),底数为2
-
PriorityQueue****底层使用堆数据结构
-
PriorityQueue****默认情况是小堆---即每次获取到的元素都是最小的元素
3.2 PriorityQueue常用接口
- 优先级队列的构造
以上只是列出了PriorityQueue中常见的几种构造方式
//创建空的优先级队列,底层默认容量是11 PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(); //创建空的优先级队列,底层的容量为initialCapacity-》100 PriorityQueue<Integer> priorityQueue2 = new PriorityQueue<>(100);
ArrayList<Integer> list = new ArrayList<>(); list.add(4); list.add(3); list.add(2); list.add(1); //用ArrayList对象来构造一个优先级队列的对象 PriorityQueue<Integer> priorityQueue3 = new PriorityQueue<>(list); System.out.println(priorityQueue3.size());//4 System.out.println(priorityQueue3.peek());//1
默认情况下,PriorityQueue队列是小堆,如果需要大堆需要提供比较器
//自定义比较器--->升序比较器:实现Comparator接口,重写接口中的compare方法 class Imp implements Comparator<Integer> { @Override public int compare(Integer o1, Integer o2) { return o2-o1; } }
public static void main(String[] args) { //传一个升序比较器 PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Imp()); priorityQueue.offer(4); priorityQueue.offer(3); priorityQueue.offer(2); priorityQueue.offer(1); System.out.println(priorityQueue.peek());//4 }
上述创建了一个大根堆
- 插入**/删除/**获取优先级最高的元素
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 方法 | 解释 |
| boolean offer(E e) | 插入元素 e ,插入成功返回 true ,如果 e 对象为空,抛出 NullPointerException 异常,时间复杂度O() ,空间不够时候会进行扩容 |
| E peek() | 获取优先级最高的元素,如果优先级队列为空,返回 null |
| E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回 null |
| int size() | 获取有效元素的个数 |
| void clear() | 清空 |
| boolean isEmpty() | 检测优先级队列是否为空,空返回 true |
public static void main(String[] args) { int[] arr = {8,5,2,10,7}; /** * 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好,这样既不会浪费内存空间,也不会由于需要扩容使效率降低 * 否则可能需要不断扩容 * 扩容机制:开辟更大空间,拷贝元素,效率比较低 * * 创建对象时没有传递比较器,默认为null,如果没有传递容量大小,容量为默认大小-->11 */ PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(arr.length,new Imp()); for (int x:arr) { priorityQueue.offer(x); } // 打印优先级队列中有效元素个数 System.out.println(priorityQueue.size());//5 //peek() 获取优先级最高的元素 System.out.println(priorityQueue.peek());//10 //poll() 从优先级队列中删除元素 priorityQueue.poll(); // 打印优先级队列中有效元素个数 System.out.println(priorityQueue.size());//4 // 获取优先级最高的元素 System.out.println(priorityQueue.peek());//8 //clear() 将优先级队列中的有效元素删除掉,检测其是否为空 priorityQueue.clear(); if(priorityQueue.isEmpty()){ System.out.println("优先级队列已经为空!!!"); } else{ System.out.println("优先级队列不为空"); } }
以下是JDK 1.8中,PriorityQueue的扩容方式
private static final int MAX_ARRAY_SIZE = Integer . MAX_VALUE - 8 ;
private void grow ( int minCapacity ) {
int oldCapacity = queue . length ;
int newCapacity = oldCapacity + (( oldCapacity < 64 ) ?
( oldCapacity + 2 ) :
( oldCapacity >> 1 ));
if ( newCapacity - MAX_ARRAY_SIZE > 0 )
newCapacity = hugeCapacity ( minCapacity );
queue = Arrays . copyOf ( queue , newCapacity );
}
private static int hugeCapacity ( int minCapacity ) {
if ( minCapacity < 0 )
throw new OutOfMemoryError ();
return ( minCapacity > MAX_ARRAY_SIZE ) ?
Integer . MAX_VALUE :
MAX_ARRAY_SIZE ;
}
如果容量小于64时,是按照oldCapacity的2倍方式扩容;如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容;如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容。
3.3 练习
top-k问题:最大或者最小的前k个数据。
/** * top-k问题 * 最小的k个数:返回数组array中最小的k个数,任意顺序都可 * 应用场景:主要是在一组大量数据中找最小的k个数 * 一般思路:1.将数组存于优先级队列中,默认是最小堆方式 * 2.优先级队列出队k次获取的k个数即为数组中最小的k个数 * * 时间复杂度:O(n*logn),n太大时,效率会非常低 * */ public static int[] smallestK(int[] arr, int k) { int[] ret =new int[k]; if(arr == null || k== 0){ return ret; } PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(); //arr中有n个数,插入的时间复杂度O(n*logn) for (int x:arr) { priorityQueue.offer(x); } //删除的时间复杂度:O(k*logk),是一个常数 for (int i = 0; i < k; i++) { ret[i] = priorityQueue.poll(); } return ret; }
该解法并不是 topK 最好的做法,那 topk 该如何实现呢?在下文介绍
四、堆应用
4.1 PriorityQueue的实现
用堆作为底层结构封装优先级队列。
4.2 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 建堆:升序:建大根堆,降序:建小根堆。
- 利用堆删除思想来进行排序
一组记录排序码为 (5 11 7 2 3 17), 则利用堆排序方法建立的初始堆为 ()
A: (11 5 7 2 3 17) B: (11 5 7 2 17 3) C: (17 11 7 2 3 5)
D: (17 11 7 5 3 2) E: (17 7 11 3 5 2) F: (17 7 11 3 2 5)
答案: C
4.3 Top-k问题
**TOP-K问题:即求数据集合中前K****个最大的元素或者最小的元素,一般情况下数据量都比较大。**对于Top-K问题,能想到的最简单直接的方式就是排序,但是如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
-
**用数据集合中前****K个元素来建堆:**前k个最大的元素,则建小堆;前k个最小的元素,则建大堆。
-
**用剩余的****N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素,**比较完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
/** * Top-k问题 * 解决思路:1.用数据集合中前K个元素来建堆(前k个最大的元素,则建小堆;前k个最小的元素,则建大堆。) * 2.用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素 * 时间复杂度为 O(k*logk) + O((n-k)*logk)--->nlogk */
//自定义比较器--->升序比较器:实现Comparator接口,重写接口中的compare方法 class Imp implements Comparator<Integer> { @Override public int compare(Integer o1, Integer o2) { return o2-o1; } } public static int[] smallestK(int[] arr,int k) { //建立大根堆 PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Imp()); int[] ret = new int[k]; if(arr == null || k== 0){ return ret; } /** * 1.用数据集合中前K个元素来建堆 * 时间复杂度为O(k*logk) */ for (int i = 0;i <k;i++){ priorityQueue.offer(arr[i]); } /** * 2.用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素 * 时间复杂度为 O((n-k)*logk) */ for (int i = k; i < arr.length; i++) { int peek = priorityQueue.peek(); if(arr[i] < peek){ priorityQueue.poll(); priorityQueue.offer(arr[i]); } } /** * 将最小的k个数放入数组中 * 时间复杂度为O(k*logk) * 不算topK的复杂度 这个地方是整理数据 */ for (int i = 0; i < k; i++) { ret[i] = priorityQueue.poll(); } return ret; }