一、三种堆的区别:
1、 优先级队列(堆):是一种特殊的数据结构,本质上是一个完全二叉树
2、Java中的"堆":JVM持有的内存中的一个内存区域
3、操作系统中的"堆":每个进程也持有一定的内存空间,在这上面也有一个内存区域叫做堆
- 优先级队列:
优先级队列也是一个队列,但不是单纯的先进先出,而是把优先级最高的先出去,该队列内部的结构就是’堆’
- 常用接口
1、PriorityQueue
1)单纯的整形优先级队列入队和出队—按照数字大小顺序来出队
import java.util.PriorityQueue;
public class Test {
public static void main(String[] args) {
//创建一个优先级队列
PriorityQueue<Integer> queue=new PriorityQueue<>();
//创建一个数组
int[] arr={4,6,1,2};
for(int x:arr){
//入队列
queue.offer(x);
}
while(!queue.isEmpty()){
//出队
System.out.println(queue.poll());
}
}
}
2)对象在一个类中的比较
例如:创建一个BoyFriend类
import java.util.PriorityQueue;
class BoyFriend{
public String name;
public int age;
public int money;
public int face;
public BoyFriend(String name, int age, int money, int face) {
this.name = name;
this.age = age;
this.money = money;
this.face = face;
}
}
public class Test {
public static void main(String[] args) {
BoyFriend[] arr={
new BoyFriend("张三",20,10,100),
new BoyFriend("李四",40,80,50),
new BoyFriend("王五",50,100,10),
};
//创建一个优先级队列
PriorityQueue<BoyFriend> queue=new PriorityQueue<>();
for(BoyFriend x:arr){
queue.offer(x);
}
while(!queue.isEmpty()){
BoyFriend cur=queue.poll();
System.out.println(cur.name);
}
}
}
修改结果:
1)利用Comparable接口—哪个类需要比较,就让这个类实现该接口(只能实现只有一种比较规则的情况—即只能用一次(eg:money))
class BoyFriend implements Comparable<BoyFriend>{
public String name;
public int age;
public int money;
public int face;
public BoyFriend(String name, int age, int money, int face) {
this.name = name;
this.age = age;
this.money = money;
this.face = face;
}
//实现Comparable里的抽象方法
@Override
public int compareTo(BoyFriend o) {
//将当前对象和o进行比较
//注意:此处的减数和被减数不用刻意去记,可根据结果进行调整
//根据钱的多少来比较
return o.money-this.money;
}
}
2)利用 Comparator接口—定义一个新的类,实现该接口,里面的compare方法的参数是两个对应要比较的类(更加适用于指定了多种比较规则的情况—可创建多个Comparator,用到哪个的时候就使用哪个)
//接口里的参数是要被比较的类
class BoyFriendComparator implements Comparator<BoyFriend>{
@Override
public int compare(BoyFriend o1, BoyFriend o2) {
return o1.money-o2.money;
}
}
二、 优先级队列的应用
1、堆
1)堆的分类:
分为大堆、小堆
将根节点最大的堆叫做最大堆或者大根堆,将根节点最小的堆叫做最小堆或小根堆
2)堆的性质:
.堆中某个节点的值总是不大于或者不小于其父节点的值
.堆总是一颗完全二叉树
3)用途:
能够高效的获取到最大值/最小值,也能高效获取到第二大/第二小…
4)堆的存储
堆实质上是一个完全二叉树,一般是使用数组(下标从0开始)来存储这个二叉树的
对于这种存储方式,寻找双亲结点和孩子结点的方式(直接计算下标):
…如果i=0,则i表示的结点为根节点,否则i的双亲结点为(i-1)/2
…如果2i+1小于节点个数,则节点i的左孩子下标为2i+1,否则没有左孩子
…如果2i+2小于节点个数,则节点i的右孩子下标为2i+2,否则没有右孩子
…最后一个节点的父节点下标为:((length-1)-1)/2
2、堆的创建
给定一个数组,整理成堆这样的结构,即转成完全二叉树并且使用堆的形式来存储,满足下标关系,并且满足堆(大堆/小堆特点)的关系
可以使用向上调整/向下调整实现
1)向下调整
如上图所示:0的左右子树都是大堆,此时可以进行向下调整
调整过程:
.从待调整的位置出发,取出该接待您的左右子树(通过下标换算方式)
.该例是大堆,所以找出其左右孩子中的较大值,拿当前节点与该较大值进行比较,看这两个元素的大小关系是否符合“大堆”的要求,倘若不符合,交换这两个元素
.将刚才被交换的位置作为新的起始位置, 再来找两个子节点的位置,并且找到较大值, 重复上述过程
注意:向下调整建堆时,必须是从后往前循环,从前往后是不行的
因为向下调整的前提条件是左右子树必须已经满足堆的要求了,才能向下调整根节点的位置
public class Heap {
//向下调整(是创建堆的有一个核心操作)
//前提条件:要求当前被调整节点的左右子树都已经是堆了
//参数中传入数组,数组有效元素长度、待调整的起始位置
//区别arr.length--->数组总的长度
//按照大堆方式实现
// 时间复杂度:O(logN)---(依次*2或者/2,其时间复杂度都与logN有关)
private static void shiftDown(int[] arr,int size,int index){
//待调整位置
int parent=index;
//待调整位置节点的左子树
int child=2*index+1;
while(child<size) {
//让child指向左右子树值较大者
if (child + 1 < size && child + 1 > child) {
child = child+ 1;
}
//比较待调整元素和该左子树的值,如果左子树节点的值大于待调整位置的值,交换
if (arr[child] > arr[parent]) {
int tmp = arr[parent];
arr[parent] = arr[child];
arr[child] = tmp;
} else {
break;
}
//调整之后,更新待调整位置及该位置左子树的位置,以备下次循环
parent = child;
child = 2 * parent + 1;
}
}
//使用向下调整建堆操作
//时间复杂度O(N)(数学推导出来的)
public void creatHeap(int[] array) {
//基于向下调整建堆
//(1)从后往前遍历数组,针对每个下标都去向下调整
for(int i=array.length-1;i>=0;i--){
shiftDown(array,array.length,i);
}
//(2)代码优化:实际向下调整时,可从第一个非叶子节点进行向下调整(叶子节点没有向下调整的必要)
//最后一个非叶子节点即最后一个节点的父节点,下标为(length-1)-1)/2
for(int i=((array.length-1)-1)/2;i>=0;i--){
shiftDown(array,array.length,i);
}
}
}
//二、 向上调整(以大堆为例)
public void shiftUp(int[] arr,int size,int index){
//创建一个child引用指向待调整位置
int child=index;
//创建一个parent引用指向其父节点的位置
int parent=(child-1)/2;
//当child=0时表示已经访问到根节点
while(child>0){
//比较待调整位置节点与父节点的大小
if(arr[child]>arr[parent]){
int tmp=arr[child];
arr[child]=arr[parent];
arr[parent]=tmp;
}else{
break;//满足要求,直接跳出循环
}
//更新child与parent,以便下次循环
child=parent;
parent=(child-1)/2;
}
}
//使用向上调整(基于offer)的建堆操作
public void createHeap1(int[] arr){
//最开始时,数组为空
//循环遍历数组,把元素通过offer方法插入数组即可
for(int x:arr){
offer(x);
}
}
3、堆的核心操作—基于"向上调整"和"向下调整"来展开
1)元素插入堆
//往堆中插入元素
//当前存储堆的数组
private int[] arr=new int[100] ;
private int size=0;
//该方法需要用到成员变量,所以不加static,通过该方法向存储堆的数组末尾插入元素
public void offer(int val){
//数组满时,插入失败(此处不进行扩容操作)
if(size>arr.length){
return;
}
//尾插
arr[size]=val;
size++;
//尾插完成之后进行向上调整
shiftUp(arr,size,size-1);
}
2)取堆顶元素
//获取堆顶元素
public Integer peek(){
if(size==0){
//数组为空
return null;
}
return arr[0];
}
3) 删除堆顶元素
注意:堆元素的删除操作只能是删除堆顶元素
思路:(移形换影)
将0号的元素,即堆顶元素与最后一个元素交换,然后size–,再将0号元素进行向下调整即可
//删除堆顶元素
public Integer poll(){
if(size==0){
return null;
}
//将0号元素与最后一个元素进行交换
int result=arr[0];
int tmp=arr[0];
arr[0]=arr[size-1];
arr[size-1]=tmp;
size--;
//从0号元素开始,往下进行向下调整
shiftDown(arr,size,0);
return result;
}