PriorityQueue讲解及源码解析

优先级队列是常用的数据结构之一,今天就来解析一下底层源码是如何实现的

知识准备

  在谈优先级队列之前,先来大概陈述一下,队列大家都知道,先入先出,每一个元素在队列中排好队,然后按照顺序进行出队,那么优先级队列是怎么回事呢?优先级顾名思义,就是给每个元素加上了优先级,元素在进入队列后,并不是先入先出了,而是需要根据元素的优先级,每次出队都是优先级大的元素出队。

  在jdk的源码中,优先级队列是通过堆来进行实现的,相信了解堆排序的朋友理解起来会更加容易,堆又是什么呢?堆有大根堆和小根堆之分,大根堆就是优先级越高,出队就优先,小根堆相反。其实堆就是二叉树,只不过有一条性质:以小根堆来说,小根堆的根节点要比两个孩子节点的优先级低,画一幅图来理解一下:
在这里插入图片描述
  上图就是一个小根堆,3的孩子7和4都比3要小,7的孩子20和12都比7小,这就是小根堆,理解起来是不是很容易呢

使用介绍

  在剖析源码之前,首先来讲一下如何去使用,jdk已经为我们提供好了很多接口,使用的时候,只需要new出来调用接口即可,下面介绍一下这些接口

  1. public boolean add(E e) 增加元素,底层直接调用offer方法
  2. public boolean offer(E e) 增加元素
  3. public boolean remove(Object o) 删除元素
  4. public E peek() 获得堆顶元素,也就是二叉树的根节点
  5. public E poll() 获得堆顶元素,获得后会删除堆顶元素

  这里先告诉大家,该数据结构的底层是数组实现的,那么乱序插入一些数字后,数组的情况是怎样的?我按照20,15,12,7,4,3,5的顺序插入,那么按照性质 ,插入后是怎么样的呢?
在这里插入图片描述
通过这些下标逻辑上转为一棵树,如下图:
在这里插入图片描述
和预想的一样,完全符合小根堆的性质,那么如何实现的呢,就要通过源码解析来深度理解一下了

源码解析
继承结构

在这里插入图片描述
继承了抽象的队列,实现了序列化接口

数据结构

摘选了四个重要的属性

	//1.数组
	transient Object[] queue; // non-private to simplify nested class access

    /**
     * The number of elements in the priority queue.
     */
    //2.元素个数
    private int size = 0;

    /**
     * The comparator, or null if priority queue uses elements'
     * natural ordering.
     */
	//3.比较器
    private final Comparator<? super E> comparator;

    /**
     * The number of times this priority queue has been
     * <i>structurally modified</i>.  See AbstractList for gory details.
     */
    //4.版本号
    transient int modCount = 0; // non-private to simplify nested class access
  1. 刚才在上面也已经提到过了,该queue的底层是数组实现,说的就是Object数组queue
  2. size用来记录队列中元素的个数
  3. 比较器就是用来进行优先级的比较,是实现优先级队列的关键
  4. 版本号用来保证安全操作,防止在迭代过程中修改数据,快速失败机制就是通过该属性来完成
构造函数

这里我们就看最主要的构造函数:
在这里插入图片描述
无非就是初始化数组,然后初始化了构造器,如果用户没有传入,则使用默认的大小,构造器为null

增加操作

在增加元素的过程中,势必会影响根堆的性质,那么如何保证该性质的呢?一起来看一下

刚才在上面也介绍了,add和offer方法都可以用来增加元素,先看一下add方法:
在这里插入图片描述
可以看到,add方法底层直接调用了offer方法

offer方法:
在这里插入图片描述

    public boolean offer(E e) {
    	//如果传入的元素为空,抛异常
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        //如果当前元素个数大于了数组的大小,则扩容
        if (i >= queue.length)
            grow(i + 1);
        size = i + 1;
        //如果插入的是第一个元素,也就是根,那么直接赋值,如果不是,向上调整
        if (i == 0)
            queue[0] = e;
        else
        	//向上调整
            siftUp(i, e);
        return true;
    }

  offer方法的逻辑比较简单,grow方法是扩容方法,后面进行解析,这里先关注增加元素的逻辑,如果插入的是第一个元素,那么直接就放入0号位置,如果不是需要向上调整,调用siftUp函数

siftUp函数:
在这里插入图片描述
siftUp的逻辑:如果用户传入了比较器,使用siftUpUsingComparator调用比较器进行比较,如果没有,调用siftUpComparable方法,使用类默认实现的Comparable接口来进行比较,两个方法的逻辑是一样的,我们看一下siftUpComparable方法

siftUpComparable方法:
在这里插入图片描述

    private void siftUpComparable(int k, E x) {
    	//获取类的Comparable接口
        Comparable<? super E> key = (Comparable<? super E>) x;
        //向上调整,还没有调整到根就一直向上调整
        while (k > 0) {
        	//找到k的父节点下标parent
            int parent = (k - 1) >>> 1;
            //通过下标拿到父节点e
            Object e = queue[parent];
            //比较k和父节点,如果k大了就不再往上调整
            if (key.compareTo((E) e) >= 0)
                break;
            //把父节点放到k的位置,k指向父节点,这个过程其实就是把key拿出来,如果父节点大
            //就往下覆盖,父节点往上移,还大,就继续把父节点覆盖下来,直到父节点小了
            queue[k] = e;
            k = parent;
        }
        //再把key拿回来
        queue[k] = key;
    }

首先需要明白的有两点:

  1. PriorityQueue维护的是一个小根堆
  2. 通过子节点拿到父节点的下标的方法是:( 子节点的下标-1)/2,而上面用了位运算>>>1,>>>即表示除以 2的意思
删除操作

删除操作调用的remove方法,一起看一下remove方法
在这里插入图片描述
  indexof方法是定位元素在数组中的下标,如果数组中有该元素,那么返回该元素的位置下标,如果没有该元素,则返回-1。如果i等于-1,则该元素不存在,直接返回false,如果存在,调用removeAt()方法:
在这里插入图片描述

    private E removeAt(int i) {
        // assert i >= 0 && i < size;
        modCount++;
        //s为数组中最后一个元素的下标
        int s = --size;
        //如果删除的是最后一个元素的下标,那么不需要调整,直接把最后一个元素的下标置为null
        if (s == i) // removed last element
            queue[i] = null;
        else {
        	//进行调整,moved为数组中的最后一个元素
            E moved = (E) queue[s];
            //把最后一个元素置为空
            queue[s] = null;
            //向下调整,该函数会先假设把最后一个元素放到删除的位置,然后从该位置开始向下调整
            //这个函数下面会详细讲解,这里理解大体做了什么即可
            siftDown(i, moved);
            //如果是第一种情况,也就是尝试的放成功了,那只是向下来说没有问题,向上还不能保证
            //所以需要向上调整
            if (queue[i] == moved) {
                siftUp(i, moved);
                //如果删除的位置不等于最后一个元素了,那么说明moved向上调整了,返回最后一个元素
                //这里的返回值其实没有任何作用,在上面可以看到removeAt方法的返回值就没有被使用
                if (queue[i] != moved)
                    return moved;
            }
        }
        return null;
    }

siftDown:
和向上调整相同,这里还是只解析一个函数的逻辑:

private void siftDown(int k, E x) {
		//根据是否有比较器,调用相应的函数
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

    @SuppressWarnings("unchecked")
    private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        //向下调整的界限是第一个叶子节点
        int half = size >>> 1;        // loop while a non-leaf
        while (k < half) {
        	//拿到左孩子下标
            int child = (k << 1) + 1; // assume left child is least
            Object c = queue[child];
            //拿到右孩子下标
            int right = child + 1;
            //比较左右孩子,谁小谁为c
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
            //比较c和最后一个元素,也就是把最后一个元素插入到要删除的位置
            //不断的向下调整
            if (key.compareTo((E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = key;
    }

获得堆顶元素

peek:
在这里插入图片描述
逻辑很简单,如果元素个数等于0,返回空,不等于0返回堆顶元素,也就是数组的第一个元素

出队

在这里插入图片描述

    public E poll() {
        if (size == 0)
            return null;
        int s = --size;
        modCount++;
        E result = (E) queue[0];
        E x = (E) queue[s];
        queue[s] = null;
        if (s != 0)
            siftDown(0, x);
        return result;
    }

  这个方法和remove方法的逻辑很类似,就是删除节点,而且该节点是数组的第一个元素,所以只需要把最后一个元素补到第一个元素,然后向下调整即可。

扩容
    private void grow(int minCapacity) {
    	//原来的容量
        int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        //原来容量小于64:扩容后=原来*2+2
        //原来容量不小于64:扩容后=原来*3
        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) {
    	//如果扩容后的容量小于0,那么说明在移位的过程中出现了溢出,抛异常
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //如果不大于最大容量,那么直接为最大容量,如果大于,则为Integer的最大值
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值