目录
一、优先级队列
二、优先级队列的模拟实现
2.1 堆的概念
2.2 堆的存储方式
2.3 堆的创建
2.4 堆的插入和删除
2.5 用堆模拟实现优先级队列
三、常用接口
3.1 PriorityQueue的特性
3.2 PriorityQueue常用接口
3.3 练习
四、堆应用
4.1 PriorityQueue的实现
4.2 堆排序
4.3 Top-k问题
队列是一种先进先出的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队
列时,可能需要优先级高的元素先出队列,该场景下,使用队列显然不合适。在这种情况下,数据
结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数
JDK1.8中的优先级队列底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进
行了一些调整。
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺
序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >=
K2i+2) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最
小的堆叫做最小堆或小根堆。
性质:堆中某个节点的值总是不大于或不小于其父节点的值;堆总是一棵完全二叉树。
堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储。
注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
将元素存储到数组中后,可以根据二叉树的性质5对树进行还原。假设i为节点在数组中的下标,则有
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,将其创建成堆
堆向下调整过程(以大堆为例):
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)。
1.堆的插入
堆的插入总共有两个步骤:
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.堆的删除
堆的删除一定删除的是堆顶元素。具体如下:
1. 将堆顶元素对堆中最后一个元素交换
public int pop(){ //堆为空 if(isEmpty()){ return -1; } //获取出堆元素 int ret = elem[0]; //出堆的元素和最后一个元素交换 swap(0,usedSize-1); usedSize--; //向下调整 siftDown(0,usedSize); return ret; }
//大根堆 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; } }
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的。
使用PriorityQueue需要注意:
1. 使用时必须导入PriorityQueue所在的包
import java.util.PriorityQueue;
2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
3. 不能插入null对象,否则会抛出NullPointerException
4. 没有容量限制,可以插入任意多个元素,内部可以自动扩容
5. 插入和删除元素的时间复杂度为O(),底数为2
6. PriorityQueue底层使用堆数据结构
7. PriorityQueue默认情况是小堆---即每次获取到的元素都是最小的元素
以上只是列出了PriorityQueue中常见的几种构造方式
//创建空的优先级队列,底层默认容量是11 PriorityQueuepriorityQueue = new PriorityQueue<>(); //创建空的优先级队列,底层的容量为initialCapacity-》100 PriorityQueuepriorityQueue2 = new PriorityQueue<>(100); ArrayListlist = new ArrayList<>(); list.add(4); list.add(3); list.add(2); list.add(1); //用ArrayList对象来构造一个优先级队列的对象 PriorityQueue priorityQueue3 = new PriorityQueue<>(list); System.out.println(priorityQueue3.size());//4 System.out.println(priorityQueue3.peek());//1
默认情况下,PriorityQueue队列是小堆,如果需要大堆需要提供比较器
//自定义比较器--->升序比较器:实现Comparator接口,重写接口中的compare方法 class Imp implements Comparator{ @Override public int compare(Integer o1, Integer o2) { return o2-o1; } } public static void main(String[] args) { //传一个升序比较器 PriorityQueuepriorityQueue = new PriorityQueue<>(new Imp()); priorityQueue.offer(4); priorityQueue.offer(3); priorityQueue.offer(2); priorityQueue.offer(1); System.out.println(priorityQueue.peek());//4 }
上述创建了一个大根堆
2. 插入/删除/获取优先级最高的元素
方法 | 解释 |
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 */ PriorityQueuepriorityQueue = 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来进行扩容。
top-k问题:最大或者最小的前k个数据。
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 priorityQueue = new PriorityQueue<>();
//arr中有n个数,插入的时间复杂度O(n*logn)
for (int x:arr) {
priorityQueue.offer(x);
}
//删除的时间复杂度:O(k*logn)
for (int i = 0; i < k; i++) {
ret[i] = priorityQueue.poll();
}
return ret;
}
用堆作为底层结构封装优先级队列。
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆 :升序:建大根堆,降序:建小根堆。
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
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。对于Top-K问题,能想到的最简单直接的方式就是排序,但是如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆:前k个最大的元素,则建小堆;前k个最小的元素,则建大堆。
2. 用剩余的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 {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
}
public static int[] smallestK(int[] arr,int k) {
//建立大根堆
PriorityQueue 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