Java:优先级队列(堆)

本文介绍了优先级队列的概念,特别是堆的底层实现,包括大根堆和小根堆的特性,以及如何通过模拟实现堆数据结构,重点讲述了如何将乱序数组调整为大根堆结构。同时,还探讨了PriorityQueue接口的构造和自定义比较器的应用。
摘要由CSDN通过智能技术生成

一、初识【堆】

1、什么是【优先级队列】?

  前面的文章我们介绍过队列,队列是一种先进先出的数据结构,但是,在某些情况下,操作的数据可能需要有一个优先级来获取数据,例如优先获取队列中最大的元素,或者优先获取队列中最小的元素,单靠先进先出并无法实现这种功能,在该种情况下,使用队列明显就不合适了。

  举例来说,当一个队列分别存储11,13,23,53时,如果我们要获取队列中的最大元素,那么根据队列的规则,只能先分别在队列中弹出11,13,23后才可以获取到53。

  因此,我们由此需要一个优先级队列来实现这个功能!

2、堆的底层实现

  优先级队列有个别称,也就是【堆】,堆是由一个类实现的:PriorityQueue类(这个类实现了Queue接口)

  在学习堆之前,我们需要学习二叉树,因为它的底层就是一个二叉树,并且这棵二叉树是一颗完全二叉树!另外,这个二叉树又是使用数组模拟实现的!关于二叉树的学习我也有写过相关文章,如有兴趣可去了解!

3、堆的概念

 堆分为【大根堆】和【小根堆】!

小根堆:

所谓的小根堆,其实就是根节点的大小  小于   孩子节点的大小

整棵树都是【小根堆】的前提是:每棵子树都是【小根堆】

大根堆: 

所谓的大根堆,其实就是根节点的大小  大于   孩子节点的大小

整棵树都是【大根堆】的前提是:每棵子树都是【大根堆】

4、节点下标的规律

如图:每个节点上面的数字代表节点的下标

假设一个节点的下标为i

1、如果  i  =0;则 i 表示的节点为根节点,如果该节点不是根节点,则节点  i  的父亲节点下标为(i-1)/2

2、如果2*i +1 小于 节点的个数,则节点 i 的左孩子下标为  2* i+1;否则没有左孩子

3、如果2*i  +2 小于 节点的个数,则节点 i 的右孩子下标为2* i +2,否则没有右孩子 



二、模拟实现【堆】

首先我们先创建两个类,TestHeap类(模拟实现堆这个类)和  Test类(测试类)

1、TestHeap类的一些【基础对象】和【方法】

首先,我们要实现的堆是通过数组实现的,因此这个TestHeap的类要【创建一个数组对象】,另外,我们还需要实现一下【构造方法】 和 【初始化数组的方法】



2、实现大根堆方法

前面我们介绍过【大根堆】的性质,它需要满足两个条件:

1、根节点元素【大于】孩子节点元素,这种树称为【大根堆】

2、整棵树是大根堆的前提是:每一颗子树都是大根堆

【问题】

  那么,如果给你一个乱序的数组,如何排列数组元素,使这个数组的【逻辑结构】是【二叉树的大根堆】呢?(如果不了解什么是逻辑结构,那么一定是你看漏了,它在这篇文章【堆的概念】图中)

  其实不难,首先我们先找到这颗满二叉树的最后一颗子树的父亲节点下标parent(以下简称parent为P),在根据【节点下标规律】求出该父亲节点左右孩子的节点下标,找到孩子节点元素的最大值,同父亲节点元素比较大小,接着会出现以下两种情况:

1:如果父亲节点元素【大于或等于】孩子节点的最大值,那么不做任何操作

2:如果父亲节点元素【小于】孩子节点的最大值,那么【交换】父亲节点该孩子节点的元素,接下来操作才是难点:

  在交换完节点内容后,则有可能会出现,交换前【孩子节点树】本来已经调整为【大根堆】,交换后又不满足【大根堆的性质】,那么就要【继续调整】该孩子节点树,使其满足大根堆的性质!(看到这里如果不理解没有关系,后举一个例子,你也许会恍然大悟)

【例子】

  如图,此时这个二叉树其实是一个数组的逻辑结构图!因为该数组尚未排序,因此其不满足大根堆的性质,圆圈里面的数代表【元素的值】,圆圈下面的数子代表【元素的下标】

1、  首先,我们之前在TestHeap类中定义了【usedSize】成员变量,用来记录数组存储的元素个数。关于如何调整该堆?先从最后一颗子树,【从右到左,从下到上】的轨迹依次调整每一颗子树,使其都成为【大根堆】。

  所以,我们需要求出最后一颗子树P的节点下标,即元素为4,元素下标为3的元素! 它就是我们要调整的第一棵树!

   那么该如果求得该元素下标的值?其实我们早就知道了:P=(最后一个元素的下标-1)/2,也就是P=[  (usedSIze-1) - 1  ] / 2;

求得该P节点的孩子节点最大值为9,因此交换元素

2、  第一棵树调整完成,开始调整第二棵树,即元素为3,下标为2的元素 ,这个时候使P=P-1即可;

求得该P节点的孩子节点的最大值为7,交换元素

3、第二棵树调整完成,开始调整第三棵树,即元素为2,下标为1的元素,P=P-1; 

此时,发现P节点的孩子节点最大值为9,交换元素;交换完元素我们就会发现,第一棵树交换前是大根堆,交换后就不是大根堆了,因此,我们需要再次调整这棵树;

令P=该孩子节点的下标,重新调整!

  后面的情况就不一一在推导了,相信大家也可以自己推导出来!上面的讲解只是未来让你能更好地结合讲解理解代码,大根堆的代码实现如下! 

它分为三个方法:

第一个方法:createHeap方法利用while循环,依次调整每一颗子树(具体调整调用方法三),是实现数组调整的主体逻辑

第二个方法:swap方法即交换父亲节点和孩子节点元素

第三个方法:siftDown方法:则通过接收根节点的下标,调整以该节点为下标的整棵树,使其成为大根堆!creatHeap方法配合使用该方法,完成每一颗子树的调整,使整棵树成为大根堆!

    //实现大根堆方法:
    public void createHeap(){
        int parent=(usedSize-1-1)/2;
        for(int i=parent;i>=0;i--){
            siftDown(i,usedSize);//调用向下调整方法
        }
    }


    //交换数组元素方法:
    private void swap(int i,int j){
        int tmp=elem[i];
        elem[i]=elem[j];
        elem[j]=tmp;
    }


    //向下调整方法:
    private void siftDown(int parent,int end){
        //通过父亲节点的下标计算左孩子节点下标
        int child=2*parent+1;
        //当孩子节点的下标大于数组下标的最大值,跳出循环
        while(child<end){
            //确保child为最大孩子节点的下标
            if(child+1<end&&elem[child]<elem[child+1]){
                child++;
            }
            //调整为大根堆
            if(elem[parent]<elem[child]){
                swap(child,parent);//调用交换数组元素方法
                parent=child;
                child=2*parent+1;
            }else{
                break;
            }
        }
    }


3、添加元素方法:

在给这个数组添加元素的时候,由于数组的【逻辑结构】要满足大根堆的性质,因此,在添加完元素过后仍然需要检查一下该数组的元素顺序。

如图为例:

  以下是一个排序为大根堆的数组的【逻辑结构】(黄色图标元素80为我们要添加的元素),现在我们要给该数组末尾太添加一个元素80,添加80后,我们发现这棵二叉树不满足大根堆的结构了!因此,我们需要对此做出调整!

调整原理!

首先,我们调整的主体逻辑是【向上调整】,即从最下面的子树开始,依次向上调整。具体实现如下:

1、先创建一个siftUp(int child)方法,给该方法传入数组最后一个元素的下标(以下简称孩子节点下标为C),此时这个下标正指向二叉树的最后一棵树的孩子节点,接着计算出父亲节点的下标(以下简称父亲节点下标为P);

比较父亲节点和孩子节点元素的大小,发现孩子节点的元素【大于】父亲节点的元素,交换元素;

将P的值【赋值】给C,即C=P,使P指向父亲节点的父亲节点的下标,即P=(C-1) / 2;

2、比较父亲节点和孩子节点元素的大小,发现父亲节点的元素【小于】孩子节点的元素 ,交换元素;

接着C=P,P=(C-1) /  2;

3、比较父亲节点和孩子节点元素的大小,发现父亲节点的元素【小于】孩子节点的元素 ,交换元素;

接着C=P,P=(C-1) /  2;

发现此时P<0,因此调整结束!

代码实现:

 //插入新数据方法:
    public void offer(int val){
        //1、如果数组满了,扩容
        if(isFull()){//调用判断数组空间已满的方法
            elem=Arrays.copyOf(elem,2*elem.length);
        }
        //2、添加元素
        elem[usedSize]=val;
        usedSize++;
        //3、调整数组顺序使其插入新数据后仍然满足大根堆
        siftUp(usedSize-1);
    }

    //判断数组空间是否已满的方法
    private boolean isFull(){
       return usedSize== elem.length;
    }

    //向上调整方法
    private  void siftUp(int child){
        int parent=(child-1)/2;
        while(parent>=0){
            if(elem[child]>elem[parent]){
                swap(child,parent);
                //使child指向该孩子节点的父亲节点
                child=parent;
                //计算出该父亲节点的父亲节点下标
                parent=(child-1)/2;
            }else{
                break;//注意注意注意!
            }

        }
    }

 在这里有一个点需要注意,那就是siftUp方法循环里面的else语句的作用,让我来举一个例子;

假设这里我们要添加的元素不是80,而是8;

此时if……else……语句走else语句,跳出while循环,因为此时这个二叉树添加8这个元素后仍然是大根堆,不需要调整!



4、删除元素方法: 

  【优先级队列】删除元素时,是删除二叉树的根节点元素,因为该元素是整个数组中的【最大值】或者【最小值】,那么该如何执行该操作?

  只需要将【数组第一个元素】和【数组最后一个元素】交换,usedSize--,最后对【下标为0】的树进行一次向下调整即可!

1、

2、 

代码实现: 

  //删除元素方法:
    public int  poll(){
        //如果数组为空,返回-1
        if(isEmpty()){
            return -1;
        }
        //数组不为空,执行删除操作
        int old=elem[0];
        swap(0,usedSize-1);//交换数组第一个元素和最后一个元素的值
        usedSize--;
        siftDown(0,usedSize);//向下调整第一棵树
        return old;
    }
    
    //判断数组是否为空方法
    public boolean isEmpty(){
        return usedSize==0;
    }
}



三、PriorityQueue的常见接口介绍

1、优先级队列的构造

priorityQueue的构造方法有三种:

构造方法:                                  功能介绍:
PriorityQueue()                            创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity)         创建一个初始容量为initialCapacity的优先级队列
PriorityQueue(Collection<? extends E> c)   用一个集合来创建优先级队列

 实例:

 //创建一个空的优先级队列,底层默认容量为11
 PriorityQueue<Integer> q1=new PriorityQueue<>();

 //创建一个空的优先级队列,底层的容量为initialCapacity
 PriorityQueue<Integer> q2=new PriorityQueue<>(100);

 //以一个集合为参数
 ArrayList<Integer> list=new ArrayList<>();
 PriorityQueue<Integer> q3=new PriorityQueue<>(list);


2、比较器

  其实,PriorityQueue默认情况下的优先级队列是【小根堆】, 让我们来看看!

首先,创建一个优先级队列q1,给该队列添加两个元素,分别是:2 、 3

如果该队列是小根堆,那么会打印出来2;反之,如果该队列是大根堆,那么会打印出来3;

运行代码,发现打印出来的是2,说明该队列默认是小根堆! 

那么,就有一个问题,如果我们要使该队列是一个大根堆,该怎么做呢?

答:在构造方法中传入一个【比较器】!

//自定义实现一个【比较器】
class IntCmp implements Comparator<Integer>{
   public int compare(Integer o1,Integer o2){
       return o2.compareTo(o1);
   }
}

public class Test {
    public static void main(String[] args) {
     //给构造方法中传入一个【比较器】
        PriorityQueue<Integer> q1=new PriorityQueue<>(new IntCmp());
        q1.offer(2);
        q1.offer(3);
        System.out.println(q1.poll());


    }
}

 

 

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值