PriorityQueue(优先级队列)的解读与底层实现

 

目录

1.什么是优先级队列?

2.优先级队列的特性

3. 如何使用优先级队列

4.JDK源码分析

4.1类的继承关系

4.2类的属性

4.3常用的方法

5.自定义优先级队列实现

5.1 优先级队列类定义

5.2 add()方法 

5.3 remove()方法 

5.4 removeAt()方法

5.5 完整代码


1.什么是优先级队列?

普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。

在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出(first in, largest out)的行为特征。通常采用堆数据结构来实现。

根据jdk源码中的描述:

优先级队列表示为平衡二进制堆:队列[n]的两个子级是队列[2*n+1]和队列[2*(n+1)]。

优先级队列由comparator排序,如果comparator为null,则由元素的自然顺序排序:对于堆中的每个节点n和n的每个后代d,n<=d。假设队列不为空,则值最低的元素位于队列[0]中。

2.优先级队列的特性

  • PriorityQueue是非线程安全,PriorityBlockingQueue是线程安全的,基于BlockingQueue接口实现。
  • 优先级队列就相当于有一个任务调度系统 给每一个任务排一个优先级 所有的任务放到优先级队列 执行任务的线程会从队列中选择一个优先级最高的任务执行。
  • PriorityQueue基于优先堆(大根堆/小根堆),在默认情况下使用的是小根堆,这个优先队列可以默认自然排序或者通过提供的Compartor(比较器)在队列实例化的时候进行排序。
  • 不允许null元素 不支持non-comparable元素

3. 如何使用优先级队列

分别创建了一个普通队列和一个优先级队列,优先级队列的排序采用的是Person类中的id大小进行排序的。

class Person{
    private int id;
    private String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
public class TestDemo7 {
    public static Comparator<Person> idComparator = new Comparator<Person>() {
        @Override
        public int compare(Person o1, Person o2) {
            return o2.getId() - o1.getId();
        }
    };

    public static void main(String[] args) {
        //练习1:
        Queue<Integer> integerPriorityQueue = new PriorityQueue<>();
        for(int i=0; i<10; i++){
            integerPriorityQueue.add(new Integer(new Random().nextInt(100)));
        }
        for(int i=0; i<10; i++){
            Integer in = integerPriorityQueue.remove();
            System.out.println("priority queue: "+in);
        }
        //练习2:
        Queue<Person> personPriorityQueue = new PriorityQueue<>(idComparator);
        for(int i=0; i<10; i++){
            int id=new Random().nextInt(100);
            personPriorityQueue.add(new Person(id, "person"+id));
        }

        for(int i=0; i<10; i++){
            Person per = personPriorityQueue.remove();
            System.out.println("person priority queue: "+per);
        }
    }
}

 

4.JDK源码分析

4.1类的继承关系

  • PriorityQueue基于Queue接口实现,在Queue接口和PriorityQueue具体实现类间提供了一个AbstractQueue抽象类
  • PriorityQueue是一个

4.2类的属性

  • private static final int DEFAULT_INITIAL_CAPACITY = 11;//默认容量
  • transient Object[] queue;//底层实现数组
  • private int size = 0;//有效元素个数
  • private final Comparator<? super E> comparator;//按照比较器对象进行排序
  • transient int modCount = 0;//修改次数

4.3常用的方法

  • add()与offer()

add()和offer()都是向队列中添加一个元素。一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,调用 add() 方法就会抛出一个 unchecked 异常,而调用 offer() 方法会返回 false,因此就可以在程序中进行有效的判断。

  • remove()与poll()

remove() 和 poll() 方法都是从队列中删除第一个元素。如果队列元素为空,调用remove() 的行为与 Collection 接口的版本相似会抛出异常,但是新的 poll() 方法在用空集合调用时只是返回 null,因此新的方法更适合容易出现异常条件的情况。

  • grow()扩容函数,扩的容量为minCapacity

对于这里的扩容,为了让数组的长度为一个素数,从而保证为一个完全二叉树。 

  • element()和peek()

element()peek()的语义完全相同,都是获取但不删除队首元素,也就是队列中权值最小的那个元素,二者唯一的区别是当方法失败时前者抛出异常,后者返回null。根据小顶堆的性质,堆顶那个元素就是全局最小的那个;由于堆用数组表示,根据下标关系,0下标处的那个元素既是堆顶元素。所以直接返回数组0下标处的那个元素即可。

  • siftDownUsingComparator()

k为根节点,用c保存左孩子,左孩子k*2+1,右孩子k*2+2,如果右孩子存在并且右孩子要比左孩子小,将右孩子赋给c,并进行下标更新,之后与x进行比较,如果x还小就break不用进行调整,否则就将c给k位置,然后进行位置更新,最后将x赋值给k位置。

 

5.自定义优先级队列实现

5.1 优先级队列类定义

优先级队列基于数据结构堆(默认小根堆)去实现,而堆常用数组实现,因此在类中定义一个泛型数组,size为有效元素个数,初始容量设为5。

public class MyPriorityQueue<E extends Comparable<E>> {
    private E[] queue;
    private int size;
    private static final int initalCapacity = 5;
    public MyPriorityQueue(){
        this(initalCapacity);
    }

    public MyPriorityQueue(int initalCapacity){
        queue = (E[])new Comparable[initalCapacity];
    }
}

5.2 add()方法 

该方法用于向优先级队列中添加元素。

1).在进行添加元素的时候,首先要判断当前容量是否已满,如果满了需要进行扩容。

2).当队列为空时,直接赋值给首位元素,szie自增

3). 正常情况下,添加元素后,可能会导致堆不是小根堆,因此利用adjustUP()进行向上调整。

因为底层基于数组,0号元素就是顶点,size-1为最后一个节点。

由子节点下标推父节点下标,当子节点下标为index时,父节点为(index-1)/2.

利用while循环对当前位置节点与其父节点依次进行比较,若父节点大于当前value,将父节点值给当前位置,然后记录父节点下标继续判断,若父节点不大于当前节点值,则现在已经是小根堆,不必调整了,循环结束后将value赋值给index位置。

public void add(E value){
        //容量不足扩容
        if(size == queue.length){
            queue = Arrays.copyOf(queue, queue.length<<1);
        }
        //队列为空
        if(size == 0){
            queue[0] = value;
            size++;
        }
        else{
            adjustUP(size, value);
            size++;
        }
    }


    public void adjustUP(int index, E value){
        //向上调整
        while(index > 0){
            //由子节点推父节点下标
            int parentIndex = (index-1)/2;
            //若父节点大于当前value,将父节点值给当前位置,然后记录父节点下标继续判断
            if(queue[parentIndex].compareTo(value) > 0){
                queue[index] = queue[parentIndex];
                index = parentIndex;
            }
            //若父节点不大于当前节点值,则现在已经是小根堆,不必调整了
            else{
                break;
            }
        }
        //将value赋给index位置
        queue[index] = value;
    }

5.3 remove()方法 

该方法用于删除根节点,同add方法类似,首先对队列进行判空,然后考虑队列只有一个元素的情况。

在正常情况下,要保证底层维护的是一个小根堆,删除的是0号位置元素,因此要把最后一个节点放到0号位置。

首先将0号位置置空,然后进行向下调整。

在向下调整的过程中,由于并不知道根节点的左右孩子哪个值较小,因此要进行判断,并分别用minIndex和minValue记录较小值的下标和值,然后将value与这个较小值进行比较,如果根节点小于minValue直接break,不用调整了,本身就还是小根堆,否则就将minValue赋值给index位置,然后进行下标更新。循环结束后将value赋值给queue[index]。

public boolean remove(){
        //删除根节点元素
        //判空
        if(size == 0){
            return false;
        }
        //当前队列只有一个元素
        if(size == 1){
            queue[size-1] = null;
            return true;
        }
        //正常情况,保证底层维护的是一个小根堆,删除的是0号位置元素,把最后一个节点放到0号位置
        queue[0] = null;
        adjustDown(0,queue[--size]);
        return true;
    }

    public void adjustDown(int index, E value){
        //向下调整
        while (index < (size>>1)){
            int leftChild = index*2 + 1;
            int rightChild = index*2 + 2;
            //保存两个孩子中最小孩子的下标
            int minIndex = 0;
            //保存当前的最小值
            E minValue = null;
            //找左右孩子的最小值
            if(rightChild < size && queue[leftChild].compareTo(queue[rightChild]) >= 0){
                minValue = queue[rightChild];
                minIndex = rightChild;
            }
            if(value.compareTo(minValue) < 0){
                break;
            }
            queue[index] = minValue;
            index = minIndex;
        }
        queue[index] = value;
    }

5.4 removeAt()方法

   public boolean remove(E target){
        //删除指定元素
        int index = -1;
        //找到target所在的位置点
        for (int i = 0; i < size; i++) {
            if(target.equals(queue[i])){
                index = i;
                break;
            }
        }
        //保证队列中有这样的位置点
        if(index == -1){
            return false;
        }
        //特殊情况,删除的是最后一个元素
        if(size-1 == index){
            queue[--size] = null;
            return true;
        }
        //正常情况下
        else {
            queue[index] = null;
            adjustDown(index, queue[size - 1]);
            //特殊情况
            if (queue[index] == queue[size - 1]) {
                adjustDown(index, queue[size - 1]);
            }
            queue[--size] = null;
            return true;
        }
    }

5.5 完整代码

package sefl.January;

import java.util.Arrays;

/**
 * Description :
 * Created by Resumebb
 * Date :2021/1/7
 */
public class MyPriorityQueue<E extends Comparable<E>> {
    private E[] queue;
    private int size;
    private static final int initalCapacity = 5;
    public MyPriorityQueue(){
        this(initalCapacity);
    }

    public MyPriorityQueue(int initalCapacity){
        queue = (E[])new Comparable[initalCapacity];
    }

    public void add(E value){
        if(size == queue.length){
            queue = Arrays.copyOf(queue, queue.length<<1);
        }
        if(size == 0){
            queue[0] = value;
            size++;
        }
        else{
            adjustUP(size, value);
            size++;
        }
    }


    public void adjustUP(int index, E value){
        //向上调整
        while(index > 0){
            //由子节点推父节点下标
            int parentIndex = (index-1)/2;
            //若父节点大于当前value,将父节点值给当前位置,然后记录父节点下标继续判断
            if(queue[parentIndex].compareTo(value) > 0){
                queue[index] = queue[parentIndex];
                index = parentIndex;
            }
            //若父节点不大于当前节点值,则现在已经是小根堆,不必调整了
            else{
                break;
            }
        }
        //将value赋给index位置
        queue[index] = value;
    }

    public boolean remove(){
        //删除根节点元素
        //判空
        if(size == 0){
            return false;
        }
        //当前队列只有一个元素
        if(size == 1){
            queue[size-1] = null;
            return true;
        }
        //正常情况,保证底层维护的是一个小根堆,删除的是0号位置元素,把最后一个节点放到0号位置
        queue[0] = null;
        adjustDown(0,queue[--size]);
        return true;
    }

    public void adjustDown(int index, E value){
        //向下调整
        while (index < (size>>1)){
            int leftChild = index*2 + 1;
            int rightChild = index*2 + 2;
            //保存两个孩子中最小孩子的下标
            int minIndex = 0;
            //保存当前的最小值
            E minValue = null;
            //找左右孩子的最小值
            if(rightChild < size && queue[leftChild].compareTo(queue[rightChild]) >= 0){
                minValue = queue[rightChild];
                minIndex = rightChild;
            }
            if(value.compareTo(minValue) < 0){
                break;
            }
            queue[index] = minValue;
            index = minIndex;
        }
        queue[index] = value;
    }

    public boolean removeAt(E target){
        //删除指定元素
        int index = -1;
        //找到target所在的位置点
        for (int i = 0; i < size; i++) {
            if(target.equals(queue[i])){
                index = i;
                break;
            }
        }
        //保证队列中有这样的位置点
        if(index == -1){
            return false;
        }
        //特殊情况,删除的是最后一个元素
        if(size-1 == index){
            queue[--size] = null;
            return true;
        }
        //正常情况下
        else {
            queue[index] = null;
            adjustDown(index, queue[size - 1]);
            //特殊情况
            if (queue[index] == queue[size - 1]) {
                adjustDown(index, queue[size - 1]);
            }
            queue[--size] = null;
            return true;
        }
    }

    public String toString(){
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < size; i++) {
            stringBuilder.append(queue[i] +" ");
        }
        return stringBuilder.toString();
    }

    public static void main(String[] args) {
        MyPriorityQueue<Integer> queue = new MyPriorityQueue<>();
        queue.add(3);
        queue.add(5);
        queue.add(7);
        queue.add(2);
        queue.add(4);
        queue.add(1);
        System.out.println(queue.toString());
        queue.remove();
        System.out.println(queue.toString());
        queue.removeAt(5);
        System.out.println(queue.toString());
    }
}

测试: 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值