文章目录
1.优先级队列
1.1 概念
前面介绍过队列, 队列是一种先进先出(FIFO)的数据结构,但有些情况下, 操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,这种场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如 果有来电,那么系统应该优先处理打进来的电话。
在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。 这种数据结构就是优先级队列(Priority Queue)。
2.优先级队列的模拟实现
JDK1.8中的PriorityQueue底层使用了堆的数据结构,而堆实际就是在完全二叉树的基础之上进行了一些元素的调 整。
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 堆的创建
建堆时,从每一个非叶子节点开始,倒着一直到根节点,都要执行一次向下调整算法。
2.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。调整:向下调整。从每棵子树的根节点开始进行调整。每棵子树的调整属于向下调整。
2.3.2 创建大根堆
//建堆:大根堆
public void createHeap(){
for(int parent=(usedSize-1-1)/2;parent>=0;parent--){
shiftDown(parent,usedSize);
}
}
//判断左右孩子谁大,前提是必须有右孩子
if(child+1<len && elem[child]<elem[child+1]){
child++;//此时保存了最大值的下标
}
private void swap(int[] array,int i,int j){
int tmp=array[i];
array[i]=array[j];
array[j]=tmp;
}
import java.util.Arrays;
/**
* @author Susie-Wen
* @version 1.0
* @description:
* @date 2022/7/15 10:29
*/
public class TestHeap {
public int[] elem;//堆的底层是数组
public int usedSize;//当前堆中有效元素的数据个数
//提供一个构造方法
public TestHeap(){
this.elem=new int[10];
this.usedSize=0;
}
public void initArray(int[] array){
elem= Arrays.copyOf(array,array.length);
usedSize= elem.length;
}
//建堆:大根堆
public void createHeap(){
for(int parent=(usedSize-1-1)/2;parent>=0;parent--){
shiftDown(parent,usedSize);
}
}
/**
*实现向下调整
* @param parent 每棵子树的根节点下标
* @param len 每棵子树的结束位置
*/
private void shiftDown(int parent,int len){
int child=2*parent+1;//使用公式
//起码得有左孩子才能进入循环
while(child<len){
//判断左右孩子谁大,前提是必须有右孩子
if(child+1<len && elem[child]<elem[child+1]){
child++;//此时保存了最大值的下标
}
if(elem[child]>elem[parent]){
swap(elem,child,parent);
parent=child;
child=2*parent-1;
}else{
break;
}
}
}
private void swap(int[] array,int i,int j){
int tmp=array[i];
array[i]=array[j];
array[j]=tmp;
}
}
public class Test{
public static void main(String[] args) {
TestHeap testHeap=new TestHeap();
int[] array={27,15,19,18,28,34,65,49,25,37};
testHeap.initArray(array);
testHeap.createHeap();
System.out.println("nlnancaklc");//在此处设置断点
}
}
2.3.3 建堆时间复杂度
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是 近似值,多几个节点不影响最终结果):
- 需要移动的节点的次数就是时间复杂度。
因此: 建堆的时间复杂度为O(N)。
题目:
2.4 堆的插入和删除
2.4.1堆插入
插入操作需要执行向上调整算法。
例如:向大根堆当中插入一个数字80,插入数据之后,必须保证仍然是一个大根堆。
public void offer(int x){
if(isFull()){
//如果数组满了,就进行扩容
elem=Arrays.copyOf(elem,elem.length*2);
}
this.elem[usedSize]=x;//将要插入的元素放到最末尾去
usedSize++;
shiftUp(usedSize-1);
}
//插入元素:向上调整
public void shiftUp(int child){
int parent=(child-1)/2;
while(child>0){
if(elem[child]>elem[parent]){
swap(elem,child,parent);
child=parent;
parent=(child-1)/2;
}else{
break;
}
}
}
public boolean isFull(){
return usedSize==elem.length;
}
2.4.2堆删除
堆删除:删的是堆顶元素,常见操作是将堆顶元素与堆中最后一个元素交换,然后堆中元素个数减少一个,重新将堆顶元素往下调整。
//出队:删除元素
public int poll(){
if(isEmpty()){
return -1;//这里写一个异常也可以
}
int old=elem[0];//记录下来要删的值
swap(elem,0,usedSize-1);
usedSize--;
shiftDown(0,usedSize);
return old;
}
public boolean isEmpty(){
return usedSize==0;
}
3.常用接口介绍
3.1 PriorityQueue的特性
JAVA当中的PriorityQueue底层默认是一个小根堆。
public class Test02 {
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue=new PriorityQueue<>();
priorityQueue.offer(12);
priorityQueue.offer(5);
priorityQueue.offer(42);
priorityQueue.offer(8);
System.out.println(priorityQueue.peek());
System.out.println(priorityQueue.poll());
}
}
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列, PriorityQueue是线程不安全的, PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
3.2 PriorityQueue使用的注意事项
- 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
- PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常。
比如:里面的元素放的是学生类
class Student implements Comparable<Student>{
public int age;
@Override
public int compareTo(Student o) {
return this.age-o.age;
}
}
public class Test03 {
public static void main(String[] args) {
PriorityQueue<Student> priorityQueue=new PriorityQueue<>();
priorityQueue.offer(new Student());
priorityQueue.offer(new Student());
}
}
- 不能插入null对象,否则会抛出NullPointerException。
- 没有容量限制,可以插入任意多个元素,其内部可以自动扩容。
- 插入和删除元素的时间复杂度为
- PriorityQueue底层使用了堆数据结构(注意:是小根堆)。
- PriorityQueue默认情况下是小堆————即每次获取到的元素都是最小的元素。
3.3 PriorityQueue的构造方法
此处只是列出了PriorityQueue中常见的几种构造方式:
构造器 | 功能介绍 |
---|---|
PriorityQueue() | 创建一个空的优先级队列,默认容量是11 |
PriorityQueue(intinitialCapacity) | 创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛IllegalArgumentException异 常 |
PriorityQueue(Collection<? extends E> c) | 用一个集合来创建优先级队列 |
class Student implements Comparable<Student>{
public int age;
@Override
public int compareTo(Student o) {
return this.age-o.age;
}
}
class AgeComparator implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return o1.age-o2.age;
}
}
public class Test03 {
public static void main(String[] args) {
AgeComparator ageComparator=new AgeComparator();
PriorityQueue<Student> priorityQueue=new PriorityQueue<>(ageComparator);
priorityQueue.offer(new Student());
priorityQueue.offer(new Student());
}
}
3.4 PriorityQueue的offer方法
3.5 PriorityQueue函数介绍
函数名 | 功能介绍 |
---|---|
boolean offer(E e) | 插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度 O(log2N),注意:空间不够时候会进行扩容 |
E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null |
E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
int size() | 获取有效元素的个数 |
void clear() | 清空 |
boolean isEmpty() | 检测优先级队列是否为空,空返回true |
3.6 PriorityQueue扩容
以下是JDK 1.8中, PriorityQueue的扩容方式:
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;
}
优先级队列的扩容说明:
- 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
- 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
- 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
4.堆的应用
4.1 常见习题
-
下列关键字序列为堆的是:()
A: 100,60,70,50,32,65
B: 60,70,65,50,32,100
C: 65,100,70,32,50,60
D: 70,65,100,32,50,60
E: 32,50,100,70,65,60
F: 50,100,70,65,60,32
答案:A -
已知小根堆为8,15,10,21,34,16,12 ,删除关键字8之后需重建堆 ,在此过程中 ,关键字之间的比较次数是()
A: 1
B: 2
C: 3
D: 4
答案:C
-
一组记录排序码为(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 -
最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后 ,其结果是()
A: [3 ,2 ,5 ,7 ,4 ,6 ,8]
B: [2 ,3 ,5 ,7 ,4 ,6 ,8]
C: [2 ,3 ,4 ,5 ,7 ,8 ,6]
D: [2 ,3 ,4 ,5 ,6 ,7 ,8]
答案:C
4.2 top-k问题
top-k问题:求最大或者最小的前k个数据。比如:世界前500强公司。
对于这类问题,可以使用优先级队列。而优先级队列可以做是因为它的底层是堆,因此就相当于堆问题的应用。
class Solution {
public int[] smallestK(int[] arr, int k) {
int[] ret=new int[k];//这里创建的是k个数,不能是其他的整数
if(k==0)return ret; //这里由测试用例可知,不能直接返回null
//由于是求最小的k个元素,因此创建大小为k的大根堆
PriorityQueue<Integer> maxPQ=new PriorityQueue<>(k,new Comparator<Integer>() {
@Override //匿名内部类,重写comparator方法
public int compare(Integer o1, Integer o2) {
//这里的类型要写Integer,不能写int
return o2-o1;//这里可以看出是创建大根堆,如果变成小根堆则是o1-o2
}
});
for(int i=0;i<arr.length;i++){
if(maxPQ.size()<k){
maxPQ.offer(arr[i]);//只要堆没有满就入堆
}else{
//获取堆顶元素
int top=maxPQ.peek();
//找前k个最小的
if(arr[i]<top){
maxPQ.poll();//出堆顶元素
maxPQ.offer(arr[i]);//把当前循环到的元素放到堆中
//每次执行一次offer方法,就会重新调整一下堆,使得其堆顶元素一直是最大的
}
}
}
for(int i=0;i<k;i++){
int val=maxPQ.poll();//每次弹出一个元素
ret[i]=val;
}
return ret;
}
}
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都 不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
- 1、用数据集合中前K个元素来建堆
- 前k个最大的元素,则建小堆
- 前k个最小的元素,则建大堆
- 2、用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素,将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
4.3 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 1、建堆
- 升序:建大堆
- 降序:建小堆
- 2、利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
public void heapSort(){
int end=usedSize-1;
while(end>0){
swap(elem,0,end);shiftDown(0,end);
end--;
}
}