优先级队列(Priority Queue)
优先级队列其背后就是一个堆,也就是完全二叉树,堆通常使用顺序表存储
在做堆的题的时候,我们通常反着进行考虑
堆的存储方式
根据父亲结点和孩子结点相互求结点
完全二叉树适合使用顺序结构存储,也就是构建一个数组存放它的数据,而非完全二叉树就不适用于这种存储方式,因为其浪费空间,效率低
堆的创建
堆的向下调整
大根堆的创建
public class TestHeap {
private int[] elem;//构建一个数组来存放堆的元素
private int usedSize;//计算下标的值,便于找到对应元素
//构造函数
public TestHeap() {
this.elem =new int[10];//我们先定义初始的数组容量为10,后面如果不够我们就进行扩容
}
public void initHeap(int[] array){
//我们构建一个初始数组 ,因为elem数组里面没有元素
// 所以我们就通过array数组来向elem数组里面添加元素
for (int i = 0; i < array.length; i++) {
elem[i]=array[i];
usedSize++;//进行计数,我们来看elem数组里面有多少元素
}
}
//构建大根堆,我们使用的是向下调整
private void shiftDown(int parent,int usedSize) {
int child = (2 * parent) + 1;//左孩子的下标
while (child < usedSize) {//只要孩子的下标不超过数组的总元素数,循环可以一直进行
if (child + 1 < usedSize && elem[child] < elem[child + 1]) {
child++;
}
//为了保证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 temp=elem[i];
elem[i]=elem[j];
elem[j]=temp;
}
public void createHeap(){
for(int parent=(usedSize-1-1)/2;parent>=0;parent--){
shiftDown(parent,usedSize);
}
}
public static void main(String[] args) {
TestHeap testHeap=new TestHeap();
int[] array={10,8,6,1,3,5,9};
testHeap.initHeap(array);
System.out.println(testHeap.usedSize);
testHeap.createHeap();
System.out.println("======");
}
}
小根堆的创建
public class TestHeap{
private int usedSize;
private int[] elem;
public TestHeap() {
this.elem = new int[10];
}
private void initHeap(int[] array){
for (int i = 0; i < array.length; i++) {
elem[i]=array[i];
usedSize++;
}
}
private void shiftDown(int parent,int usedSize){
int child=(parent-1)/2;
while(child<usedSize){
if(child+1<usedSize&&elem[child]>elem[child+1]){
child++;
}//child代表的元素一定是左右孩子中最小的
if(elem[child]<elem[parent]){
swap(child,parent);
parent=child;
child=2*child+1;
}else{
break;
}
}
}
private void swap(int i,int j){
int temp=elem[i];
elem[i]=elem[j];
elem[j]=temp;
}
private void createHeap(){
for(int parent=(usedSize-1-1)/2; parent>=0; parent--){
shiftDown(parent,usedSize);
}
}
public static void main(String[] args) {
TestHeap testHeap=new TestHeap();
testHeap.initHeap(new int[]{5,7,10,15,11,8,9});
System.out.println(testHeap.usedSize);
testHeap.createHeap();
System.out.println("========");
}
}
建堆的时间复杂度
(1)当我们采用向下调整去建堆的时候,时间复杂度为:O(n)
(2)采用向上调整去建堆,时间复杂度为:O(N*logN)
堆的插入和删除
堆的插入——使用向上调整
解题关键:我们要向一个堆里面插入元素,我们就将所要插入的元素放在该堆的最后一个结点的位置,然后使用向上调整的方法对其进行检查是否是我们要的大根堆或者小根堆
import java.util.Arrays;
//大根堆的创建
public class TestHeap {
private int[] elem;//构建一个数组来存放堆的元素
private int val;
private int usedSize;//计算下标的值,便于找到对应元素
//构造函数
public TestHeap() {
this.elem =new int[10];//我们先定义初始的数组容量为10,后面如果不够我们就进行扩容
}
public void initHeap(int[] array){
//我们构建一个初始数组 ,因为elem数组里面没有元素
// 所以我们就通过array数组来向elem数组里面添加元素
for (int i = 0; i < array.length; i++) {
elem[i]=array[i];
usedSize++;//进行计数,我们来看elem数组里面有多少元素
}
}
//构建大根堆,我们使用的是向下调整
private void shiftDown(int parent,int usedSize) {
int child = (2 * parent) + 1;//左孩子的下标
while (child < usedSize) {//只要孩子的下标不超过数组的总元素数,循环可以一直进行
if (child + 1 < usedSize && elem[child] < elem[child + 1]) {
child++;
}
//为了保证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 temp=elem[i];
elem[i]=elem[j];
elem[j]=temp;
}
public void createHeap(){
for(int parent=(usedSize-1-1)/2;parent>=0;parent--){
shiftDown(parent,usedSize);
}
}
//堆的插入
//插入元素的方法
public void offer(int val){
if(isFull()){//如果已经满了,那么就进行扩容,<64 2倍扩容|>64 1.5倍扩容
this.elem= Arrays.copyOf(elem,2*elem.length);
}
this.elem[usedSize]=val;
//向上调整
shiftUp(usedSize);
usedSize++;
}
//我们向下调整是设置括号中为parent,根据parent我们来得到child的值
//而向上调整恰恰相反,我们是根据child来求parent,因为此时的child就是你插入进来放在最后的元素,所以需要根据child求出parent的值
private void shiftUp(int child) {
int parent = (child - 1) / 2;
while (child > 0) {//这个时候的第一遍的child就是所插入的那个元素的下标,即elem[usedSize]=val
if (elem[child] > elem[parent]) {
swap(child, parent);
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
public boolean isFull(){
return usedSize==elem.length;//如果是 true,则返回这个,否则就返回false
}
public static void main(String[] args) {
TestHeap testHeap=new TestHeap();
int[] array={10,8,6,1,3,5,9};
testHeap.initHeap(array);
System.out.println(testHeap.usedSize);
testHeap.createHeap();
System.out.println("======");
testHeap.offer(20);
System.out.println(testHeap.usedSize);
testHeap.createHeap();
}
}
堆的删除——使用向下调整
解题关键:删除的是栈顶元素,交换根节点和最后一个结点,然后使用向下调整
public int poll(){
int tem=elem[0];//根节点
swap(0,usedSize-1);//根节点和最后一个结点进行交换
usedSize--;
shiftDown(0,usedSize);
return tem;
}
PriorityQueue
PriorityQueue的注意事项
1.使用时必须导入PriorityQueue所在的包:
2.PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
3.不能插入null对象,否则会抛出NullPointerException
4.没有容量限制,可以插入任意多个元素,其内部可以自动扩容
5.插入和删除元素的时间复杂度为O(log2N)//以2为底
6.PriorityQueue底层使用了堆数据结构
7.PriorityQueue默认情况下是小堆
PriorityQueue的构造方法
这是在码源上的构造方法
以上PriorityQueue的构造方法中,(1)无参构造则系统自己直接给一个容量,默认为11;
(2)而第二种有参数的构造我们则可以在initialCapacity中赋值,给它自定义容量大小;
(3)第三种是 创建接口来实现不同元素之间的比较——后面我们会详细讲到
PriorityQueue的方法
注意:【1】PriorityQueue的offer方法默认建立的是小根堆,poll方法是抛出栈顶元素
【2】当我们想要的是大根堆的时候,我们需要构建比较器接口
最后一行代码return也可以写成o2-o1
PS:(1)o2-o1 大根堆
(2) o1-o2小根堆不要写反了
添加比较器之后,我们还要在这个地方写上
PriorityQueue扩容的注意事项
优先级队列的扩容说明:(1)当容量小于64时,按照oldCapacity的2倍方式扩容;
(2)当容量大于等于64,按照oldCapacity的1.5倍方式扩容;
(3)当容量超过MAX_ARRARY_SIZE,按照MAX_ARRARY_SIZE来进行扩容
堆的应用
TOP-K问题
(这个经常会在面试题中考到)
即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都较大
做法1:把数组排序,排序之后取出前K个最大的;数据量非常大,你无法在内存中排序
做法2:把所有数据放在优先级队列中,出队K次可以吗?不可以!因为数据量非常大的时候,你无法把所有数据放在优先级队列中
以上两种做法效率都不会很高,所以我们使用下面的方法来实现
做题关键:
【1】用数据集合中前K个元素来建堆
前K个最大的元素,则建小堆
前K个最小的元素,则建大堆
【2】用剩余的N-K个元素依次与栈顶元素来比较,不满足则替换栈顶元素
在数据集合中寻找前K个最大的数
public int[] smalllestK(int[] array, int k) {
int[] ret = new int[k];//这个数组用来存放前k个元素
if(array==null||k<=0){
return ret;//为什么没有返回null,是因为力扣中要求,符合if的时候输出为[]
}
PriorityQueue<Integer> priorityQueue=new PriorityQueue<>();
for (int i = 0; i < k; i++) {
priorityQueue.offer(array[i]);//将前k个元素放在优先级队列里面,默认构建小根堆
}
for (int i = k; i < array.length; i++) {
int top=priorityQueue.peek();
if(array[i]>top){
priorityQueue.poll();
priorityQueue.offer(array[i]);
}
}
//前面的for循环将最大的数都按排在前面了,我们现在再用一个for循环,将它从优先级队列中导出
for (int i = 0; i <k ; i++) {
ret[i]= priorityQueue.poll();
}
return ret;
}
在数据集合中寻找前K个最小的数
class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
}
public int[] biglestK(int[] array,int k) {
int[] ret = new int[k];
if(array==null||k<=0){
return ret;
}
PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(new IntCmp());
for (int i = 0; i <k; i++) {
priorityQueue.offer(array[i]);
}
for (int i = k; i <array.length; i++) {
int top=priorityQueue.peek();
if(array[i]<top){
priorityQueue.poll();
priorityQueue.offer(array[i]);
}
}
for (int i = 0; i <k ; i++) {
ret[i]=priorityQueue.poll();
}
return ret;
}
}
求第K大或者小的元素
那么我们跟上面的解题方法是一样的,就是最后return的时候返回k所对应的那一个值就好
堆排序
【1】要求从小到大排序:建立大根堆
【2】要求从大到小排序:建立小根堆
举个例子:建立从小到大排序
public void heapSort(){
int end=usedSize-1;
while(end>0){
swap(0,end);
shiftDown(0,end);
end--;
}
}