目录
一、优先级队列
(1)优先级队列的概念
(2)优先级队列的模拟实现
二、堆
(1)堆的概念
(2)堆的存储方式
(3)堆的创建
1.堆的向下调整
2.堆的创建
3.建堆的时间复杂度
(4)堆的操作
1.堆的插入
2.堆的删除
(5)堆的应用
1.优先级队列(PriorityQueue)的实现
2.Top-k问题
三、PriorityQueue接口
(1)PriorityQueue的特性
(2)PriorityQueue常用接口介绍
1. 优先级队列的构造
2. 优先级队列常用功能
3.优先级队列扩容方式
四、相关oj练习题
一、优先级队列
(1)优先级队列的概念
队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队
列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。
在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
(2)优先级队列的模拟实现
JDK1.8中的PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。
二、堆
(1)堆的概念
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1&&Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆中某个节点的值总是不大于或不小于其父节点的值。
堆逻辑上是一棵完全二叉树。采用顺序结构进行组织,物理存储上表现为一个数组。(不需要按照链式结构进行组织,所以没有结点概念)
(2)堆的存储方式
堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储。
注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
将元素存储到数组中后,可以根据二叉树的性质对树进行还原。
假设i为节点在数组中的下标,则有:
如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
假设有一个顺序结构维护的完全二叉树:
long[] array={...} int size=...;
问题:
1.[i]下标是否一个合法下标:
0<=i<size
2.[i]是一个合法的下标,问孩子下标是否是一个合法下标:
左孩子:0<=2*i+1<size 右孩子:0<=2*i+2<size
3.已知[i]下标所在元素,判断是否有右孩子:
0<=2*i+2<size
4.已知[i]下标所在元素,判断是否是叶子:
因为是完全二叉树,所以只需要判断该结点是否有左孩子,没有左孩子即代表也没有右孩子,判断方式就是大于数组长度。
2*i+2>=size
(3)堆的创建
1.堆的向下调整
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据。根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。
向下过程(以小堆为例):
1. 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
2. 如果parent的左孩子存在,即:child < size, 进行以下操作,直到parent的左孩子不存在
parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标
将parent与较小的孩子child比较,如果:
parent小于较小的孩子child,调整结束
否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子
树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续2。
注意前提:
待调整的元素不是叶子即代表有孩子。除了待调整的元素,其他部分均已经满足大堆或者小堆。
在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆了才可以向下调整。
时间复杂度分析:
最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为
向下调整的代码:
(这里就把数组长度array.length当做堆的大小size,实际上数组长度可以更大,只要堆的大小size<=数组长度就可以了,数组不一定都要存满。)
调整成小堆(非递归版):
public static void adjustDownSmallHeap (int array[],int index) {
int parent = index;//需要调整的元素下标
int child = 2 * parent + 1;//假设最小孩子是左孩子
//判断右孩子是否存在,找出左右孩子中最小的一个
while (child < array.length) {//孩子结点存在
if (child + 1 < array.length && array[child] > array[child + 1]) {
child += 1;//如果右孩子存在,并且更小,就把右孩子定为最小孩子
}
if (array[parent] <= array[child]) {//当待调整元素小于等于最小孩子,则代表满足小堆性质
break;//跳出循环
}
//向下调整,就是和最小孩子交换
int tmp = array[parent];
array[parent] = array[child];
array[child] = tmp;
// parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
parent = child;//改变需要调整的元素下标和最小孩子的下标
child = 2 * parent + 1;
}
}
调整成小堆(递归版):
public static void adjustDownSmallHeap递归 (int array[],int index) {
int parent = index;//需要调整的元素下标
int child = 2 * parent + 1;//假设最小孩子是左孩子
//判断右孩子是否存在,找出左右孩子中最小的一个
if(child>=array.length){
return ;
}
if (child + 1 < array.length && array[child] > array[child + 1]) {
child += 1;//如果右孩子存在,并且更小,就把右孩子定为最小孩子
}
if (array[parent] <= array[child]) {//当待调整元素小于等于最小孩子,则代表满足小堆性质
return;
}
//否则不满足堆的性质
//向下调整,就是和最小孩子交换
int tmp = array[parent];
array[parent] = array[child];
array[child] = tmp;
//递归
adjustDownSmallHeap递归(array,child);
}
调整成大堆(非递归版):(注意除了待调整的元素,其他结点元素均要满足大堆的性质,如下图除了根结点2不满足,其他左右子树都已经是满足大堆了)
public static void adjustDownBigHeap (int array[],int index) {
int parent = index;//需要调整的元素下标
int child = 2 * parent + 1;//假设最大孩子是左孩子
//判断右孩子是否存在,找出左右孩子中最大的一个
while (child < array.length) {//孩子结点存在
if (child + 1 < array.length && array[child] < array[child + 1]) {
child += 1;//如果右孩子存在,并且更大,就把右孩子定为最大孩子
}
if (array[parent] >= array[child]) {//当待调整元素大于等于最小孩子,则代表满足大堆性质
break;//跳出循环
}
//向下调整,就是和最大孩子交换
int tmp = array[parent];
array[parent] = array[child];
array[child] = tmp;
// parent中小的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
parent = child;//改变需要调整的元素下标和最大孩子的下标
child = 2 * parent + 1;
}
}
调整成大堆(递归版):
public static void adjustDownBigHeap递归 (int array[],int index) {
int parent = index;//需要调整的元素下标
int child = 2 * parent + 1;//假设最大孩子是左孩子
//判断右孩子是否存在,找出左右孩子中最大的一个
if (child + 1 < array.length && array[child] < array[child + 1]) {
child += 1;//如果右孩子存在,并且更大,就把右孩子定为最大孩子
}
if (array[parent] >= array[child]) {//当待调整元素大于等于最小孩子,则代表满足大堆性质
return;//跳出循环
}
//否则不满足堆的性质
//向下调整,就是和最大孩子交换
int tmp = array[parent];
array[parent] = array[child];
array[child] = tmp;
adjustDownBigHeap递归(array,child);
}
2.堆的创建
对于普通的序列{ 1,5,3,8,7,6 },即根节点的左右子树不满足堆的特性。调整过程如下:
简而言之就是先调整所有叶子结点的根结点部分,叶子结点那一层已经满足向下调整操作的性质,对该层调整使叶子结点那一层满足堆性质后,再看向上面一层,也就满足向下调整操作的性质,对该层操作满足堆的性质,依次往上。
建小堆:
public static void createSmallHeap (int array[]){
int parentIndex=(array.length-1-1)/2;//array.length-1是最后一个叶子结点的位置,(i-1)/2是求父亲结点的公式
for(int i=parentIndex;i>=0;i--){
adjustDownSmallHeap(array,i);
}
}
建大堆:
public static void createBigHeap (int array[]){
int parentIndex=(array.length-1-1)/2;//array.length-1是最后一个叶子结点的位置,(i-1)/2是求父亲结点的公式
for(int i=parentIndex;i>=0;i--){
adjustDownBigHeap(array,i);
}
}
3.建堆的时间复杂度
堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):建堆的时间复杂度为O(N)
(4)堆的操作
1.堆的插入
a. 先将元素放入到底层空间中(注意:空间不够时需要扩容)
b. 将最后新插入的节点向上调整,直到满足堆的性质
2.堆的删除
堆的删除一定删除的是堆顶元素。
a. 将堆顶元素对堆中最后一个元素交换
b. 将堆中有效数据个数减少一个
c. 对堆顶元素进行向下调整
(5)堆的应用
1.优先级队列(PriorityQueue)的实现
底层结构就是依靠堆的性质,其中包括堆的删除、添加元素等等。
package heap_1101;
// 使用小堆来维护
// size >= 0
// array != null
// array + size 满足小堆的性质
// 堆的定义:要求每个位置都比它的两个孩子(如果存在的话)要小
public class MyPriorityQueue {
// 元素类型使用 long 类型
// 暂时不考虑扩容的情况
private final long[] array = new long[1000];
private int size; // 元素的个数
public MyPriorityQueue(){
size=0;
}
// 查看优先级队列中最小值
// O(1)
public long peek() {
if (size <= 0) {
throw new RuntimeException("空的");
}
return array[0]; // 根的位置就是最小值
}
// 把 e 添加到堆中(优先级队列)
// 同时维护好最小堆的性质
// 最坏情况:从叶子 -> 根
// O(log(n))
public void offer(long e) {
array[size]=e;
size++;
int child=size-1;//新加入元素的下标
while(child!=0){// child == 0 说明是根,不需要调整了
int parent=(child-1)/2;//新加入元素父亲结点的下标
if(array[parent]<=array[child]){
break;
}
//交换
long tmp=array[parent];
array[parent]=array[child];
array[child]=tmp;
//向上调整
child=parent;
}
}
// 删除堆顶元素
// 同时维护好最小堆的性质
// O(log(n))
public long poll() {
if (size <= 0) {
throw new RuntimeException("空的");
}
long e = array[0];
array[0] = array[size - 1];
array[size - 1] = 0; // 没有意义,可以不需要这一步
size--;//调整堆的大小
//对根结点进行向下调整
int parent=0; // O(log(n))
int child=2*parent+1;
while(child<size){
if(child+1<size&&array[child+1]<array[child]){
child+=1;
}
if(array[child]>=array[parent]){
break;
}
long tmp=array[parent];
array[parent]=array[child];
array[child]=tmp;
parent=child;
child=2*parent+1;
}
return e;//返回删除的元素
}
public static void main(String[] args) {
MyPriorityQueue q = new MyPriorityQueue();
q.offer(3);
q.offer(9);
q.offer(7);
q.offer(2);
q.offer(6);
q.offer(8);
q.offer(5);
q.offer(4);
q.offer(3);
System.out.println(q.poll()); // 2
q.offer(1);
}
}
2.Top-k问题
Top-k问题,求取前k个最大或者最小元素。适用于海量数据当中,比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决。
基本思路:
a. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
b. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
实例运用,见oj相关练习题。
三、PriorityQueue接口
(1)PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
关于PriorityQueue的使用要注意:
1. 使用时必须导入PriorityQueue所在的包,即:import java.util.PriorityQueue;
2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出
ClassCastException异常。
3. 不能插入null对象,否则会抛出NullPointerException。
4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容。
5. 插入和删除元素的时间复杂度为
6. PriorityQueue底层使用了堆数据结构。
7. PriorityQueue默认情况下是小堆,即每次获取到的元素都是最小的元素。
(2)PriorityQueue常用接口介绍
1. 优先级队列的构造
构造器 | 功能介绍 |
PriorityQueue() | 创建一个空的优先级队列,默认容量是11 |
PriorityQueue(int initialCapacity) | 创建一个初始容量为initialCapacity的优先级队列,注意: initialCapacity不能小于1,否则会抛IllegalArgumentException异 常 |
PriorityQueue(Collection<? extends E> c) | 用一个集合来创建优先级队列 |
static void TestPriorityQueue(){
// 创建一个空的优先级队列,底层默认容量是11
PriorityQueue<Integer> q1 = new PriorityQueue<>();
// 创建一个空的优先级队列,底层的容量为initialCapacity
PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
ArrayList<Integer> list = new ArrayList<>();
list.add(4);
list.add(3);
list.add(2);
list.add(1);
// 用ArrayList对象来构造一个优先级队列的对象
// q3中已经包含了三个元素
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
System.out.println(q3.size());
System.out.println(q3.peek());
}
PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器。这里可以参考比较这篇博客。
// 用户自己定义的比较器:直接实现Comparator接口,然后重写该接口中的compare方法即可
class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
}
public class TestPriorityQueue {
public static void main(String[] args) {
PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
p.offer(4);
p.offer(3);
p.offer(2);
p.offer(1);
p.offer(5);
System.out.println(p.peek());
}
}
2. 优先级队列常用功能
函数名 | 功能介绍 |
boolean offer(E e) | 插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时 间复杂度 ,注意:空间不够时候会进行扩容 |
E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null |
E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
int size() | 获取有效元素的个数 |
void clear() | 清空 |
boolean isEmpty() | 检测优先级队列是否为空,空返回true |
static void TestPriorityQueue2(){
int[] arr = {4,1,9,2,8,0,7,3,6,5};
// 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
// 否则在插入时需要不多的扩容
// 扩容机制:开辟更大的空间,拷贝元素,这样效率会比较低
PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
for (int e: arr) {
q.offer(e);
}
System.out.println(q.size()); // 打印优先级队列中有效元素个数
System.out.println(q.peek()); // 获取优先级最高的元素
// 从优先级队列中删除两个元素之和,再次获取优先级最高的元素
q.poll();
q.poll();
System.out.println(q.size()); // 打印优先级队列中有效元素个数
System.out.println(q.peek()); // 获取优先级最高的元素
q.offer(0);
System.out.println(q.peek()); // 获取优先级最高的元素
// 将优先级队列中的有效元素删除掉,检测其是否为空
q.clear();
if(q.isEmpty()){
System.out.println("优先级队列已经为空!!!");
} else{
System.out.println("优先级队列不为空");
}
}
3.优先级队列扩容方式
如果容量小于64时,是按照oldCapacity的2倍方式扩容的
如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
四、相关oj练习题
top-k问题:最大或者最小的前k个数据。
面试题 17.14. 最小K个数
class Solution {
static class IntegerReverseComparator implements Comparator<Integer>{
public int compare(Integer o1, Integer o2){
return o2-o1;
}
}
public int[] smallestK(int[] arr, int k) {
if(k==0){
return new int [0];
}
//要找到最小的k个数,所以要建立一个最大容量为k的大堆
//java中PriorityQueue内部实现是小堆
//重新定义5<3,3>5,3=3
Comparator <Integer> c=new IntegerReverseComparator();
//传入Comparator 构建优先级队列
PriorityQueue <Integer> priorityQueue=new PriorityQueue<>(c);
for(int i=0;i<k;i++){
priorityQueue.offer(arr[i]);
}
//将剩下元素和堆顶元素比较
for(int i=k;i<arr.length;i++){
int e=arr[i];
int top=priorityQueue.peek();
//把比堆顶元素小的放入堆顶
if(e<top){
//把堆顶元素删除
priorityQueue.poll();
//放入新堆顶元素
priorityQueue.offer(e);
}
}
int []ans=new int [k];
for(int i=0;i<k;i++){
ans[i]=priorityQueue.poll();
}
return ans;
}
}