【学习集合--Queue集合实现PriorityQueue】

学习内容:

  1. 概述
  2. PriorityQueue队列基本使用
  3. PriorityQueue队列的构造
  4. PriorityQueue队列的核心工作原理
  5. 扩容操作
  6. 移除操作

学习产出:

概述

在这里插入图片描述

 PriorityQueue(优先队列)基于数组形式的小顶堆结构,保证了每次添加移除数据之后都能够维持小顶堆结构特点。


基本使用

   PriorityQueue<Integer> a=new PriorityQueue<>();
        a.add(5);
        a.add(4);
        a.add(2);
        a.add(1);
        a.add(3);
        for (int i = 0; i < a.size(); ) {
            System.out.print(a.poll()+" ");
        }
1 2 3 4 5

 要求存储的数据类型要么实现了 java.lang.Comparable 接口,要么在实例化对象时,实现了实例化时实现 java.lang.Comparator,否则会报异常。


PriorityQueue队列的构造

几个重要的属性和常量,源码如下

public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable {

    @java.io.Serial
    private static final long serialVersionUID = -7720805057305804111L;
    //初始化大小
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
    //存储数据的数组
    transient Object[] queue; // non-private to simplify nested class access
    //大小
    int size;
    //比较器
    private final Comparator<? super E> comparator;
    //操作次数,借鉴了CAS思想,在非线程安全的使用场景中实现了简单的数据安全判定
    transient int modCount;
    ···
    }

 其中comparator是实现了Comparator接口的类对象,Comparator接口是一个函数式接口,该接口要求实现compara(o1,o2)方法,用于比较两个对象的大小,第一个参数大于第二个返回正数,等于返回0,小于返回负数

PriorityQueue构造方法较多,在JDK14中有七个,其中重要的两个如下

//设置初始化容量,小于1会抛出异常
//comparator可以不传入具体的值
public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        //每个位置上的对象都为空
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }

    //创建一个包含指定第三方集合中所有数据对象引用的队列
    public PriorityQueue(Collection<? extends E> c) {
    //传入的集合实现了SortedSet接口
        if (c instanceof SortedSet<?>) {
            SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
            this.comparator = (Comparator<? super E>) ss.comparator();
       
            initElementsFromCollection(ss);
        }
        //传入的集合就是另一个PriorityQueue队列
        else if (c instanceof PriorityQueue<?>) {
            PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
            this.comparator = (Comparator<? super E>) pq.comparator();
            initFromPriorityQueue(pq);
        }
        //其他情况,不设置新的comparator 对象
        else {
            this.comparator = null;
            initFromCollection(c);
        }
    }

  第一个构造方法第一个参数用于设置初始化容量大小,如果是其他的构造函数调用的话,传入DEFAULT_INITIAL_CAPACITY 是11。如果第二个参数是Comparator接口的对象,若为空,则在降序升序操作时,会使用对象本身定义的权值比较方式,如果对象本身没有实现Comparator接口则会抛出异常。
  第二个构造方法,会根据传入的集合对性质决定处理的细节
上面初始化调用的initElementsFromCollection,initFromPriorityQueue,initFromCollection源码如下

//
private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
        //一般情况下条件都会成立,因为该私有方法只有构造方法能直接使用
        if (c.getClass() == PriorityQueue.class) {
        //将传入集合的数据对象当做当前队列的数据对象
            this.queue = ensureNonEmpty(c.toArray());
            this.size = c.size();
        } else {
            initFromCollection(c);
        }
    }

    private void initElementsFromCollection(Collection<? extends E> c) {
        //以数组的形式获取集合的所有数据
        Object[] es = c.toArray();
        int len = es.length;
        //如果是集合不是ArrayList,copyOf应该是把他转成实际的类型?有人知道麻烦私信或者留言一下,谢谢
        if (c.getClass() != ArrayList.class)
            es = Arrays.copyOf(es, len, Object[].class);
        //条件成立,对每一个元素判空
        if (len == 1 || this.comparator != null)
            for (Object e : es)
                if (e == null)
                    throw new NullPointerException();
        //将es数组指定为PriorityQueue队列的queue数组
        this.queue = ensureNonEmpty(es);
        this.size = len;
    }

    private void initFromCollection(Collection<? extends E> c) {
        initElementsFromCollection(c);
        //完成初始化操作后,queue数组中的排列方式不一定符合PriorityQueue队列的要求
        //使用该方法对数据对象进行堆排序
        heapify();
    }

  注意构造方法对传入的集合是PriorityQueue和SortedSet做了特殊处理是因为SortedSet接口的实现类内部必须实现一个comparator()方法(抽象方法)。
  除了这两种集合外,其他集合都不能保证新的PriorityQueue队列中数据的有序性


PriorityQueue队列的核心工作原理

堆升序操作

  由于PriorityQueue队列内部始终保持堆结构,所以在添加数据的时候,需要根据数据对象的权值确定数据的索引位置,使得其继续报错小顶堆的结构。
  在数组存储的最后一个位置的下一个位置添加数据对象,按照数据二叉树的降维原理,这个索引位置是二叉树的最后一个叶子节点。再添加完成之后,验证该完全二叉树是否还是一个堆结构,如果不是,则需要调整使完全二叉树重写变成小顶堆。这个过程称为堆的升序操作。代码如下

//该方法用于在完全二叉树指定的k号索引位置上添加数据对象x
//保证堆结构的稳定,会从k开始调整数据对象x到符合要求的索引位置
private void siftUp(int k, E x) {
		//根据是否传入了比较强对象,不同场景,调用不同方法
        if (comparator != null)
            siftUpUsingComparator(k, x, queue, comparator);
        else
            siftUpComparable(k, x, queue);
    }

private static <T> void siftUpComparable(int k, T x, Object[] es) {
        Comparable<? super T> key = (Comparable<? super T>) x;
        //条件成立说明在有效范围内
        while (k > 0) {
        	//拿到当前索引位的父级
            int parent = (k - 1) >>> 1;
            Object e = es[parent];
            //如果当前权值大于等于父节点,跳出循环
            if (key.compareTo((T) e) >= 0)
                break;
            //没有大于父节点,将父节点放到k位置上,因为我们这里记录了x的值,所以并不需要把x放到原先的父节点位置上
            es[k] = e;
            k = parent;
        }
        es[k] = key;
    }
 //该方法与上面类似
 private static <T> void siftUpUsingComparator(
        int k, T x, Object[] es, Comparator<? super T> cmp) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = es[parent];
            if (cmp.compare(x, (T) e) >= 0)
                break;
            es[k] = e;
            k = parent;
        }
        es[k] = x;
    }
堆的降序操作

  当从PriorityQueue队列移除数据时,会从根节点移除,然后将最后一个节点的值替换到根节点上,然后判断当前队列是否符合二叉树结构,如果不能则从根节点进行降序操作,知道完全二叉树恢复堆结构。代码如下

//一样的根据是否传入比较器,执行不同方法
private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x, queue, size, comparator);
        else
            siftDownComparable(k, x, queue, size);
    }

    private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
        // assert n > 0;
        Comparable<? super T> key = (Comparable<? super T>)x;
        //判断一个节点是否需要进行降序,一定是在非叶子节点上判定的。叶子节点无法降序
        //取到最后一个非叶子节点的索引位置
        int half = n >>> 1;           // loop while a non-leaf
        while (k < half) {
            //取左孩子节点
            int child = (k << 1) + 1; // assume left child is least
            Object c = es[child];
            //右孩子节点
            int right = child + 1;
            //right<n 是确定该节点是否有右儿子,如果有有孩子,且左孩子大于右孩子,则c取右孩子的值(总的来说是去里面小值)
            if (right < n &&
                ((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
                c = es[child = right];
            //该节点的值小于等于值较小的孩子值,跳除循环
            if (key.compareTo((T) c) <= 0)
                break;
            //和较小的孩子交换(这里和上面一样的,因为记录了值,所以没必要一定把值赋上去。理解这个意思就行了)
            es[k] = c;
            k = child;
        }
        es[k] = key;
    }

    private static <T> void siftDownUsingComparator(
        int k, T x, Object[] es, int n, Comparator<? super T> cmp) {
        // assert n > 0;
        int half = n >>> 1;
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = es[child];
            int right = child + 1;
            if (right < n && cmp.compare((T) c, (T) es[right]) > 0)
                c = es[child = right];
            if (cmp.compare(x, (T) c) <= 0)
                break;
            es[k] = c;
            k = child;
        }
        es[k] = x;
    }
小顶堆的修复性排序

  在一些场景中,队列不一定是小顶堆结构,不是一个节点的问题,而是整体的偏差(在初始化的时候,参照另一个集合的构造方法),此时使用heapify()方法进行排序。代码如下

//相当于是对所有非叶子节点进行降序操作
private void heapify() {
        final Object[] es = queue;
        int n = size, i = (n >>> 1) - 1;
        final Comparator<? super E> cmp;
        if ((cmp = comparator) == null)
            for (; i >= 0; i--)
                siftDownComparable(i, (E) es[i], es, n);
        else
            for (; i >= 0; i--)
                siftDownUsingComparator(i, (E) es[i], es, n, cmp);
    }

PriorityQueue扩容操作

扩容时,只是在尾部增加空间,对于原来的堆结构没有影响。

private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        //小于64加2,大于等于扩容一倍
        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 final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//大于
 private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

PriorityQueue队列添加操作

   有两个添加方法add(E),offer(E),offer(E)才是实际工作的方法。代码如下

public boolean add(E e) {
        return offer(e);
    }

    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
            //操作次数加一,保证线程合法性,借鉴了CAS思路
        modCount++;
        //当前实际容量和数组大小比较,达到上限进行扩容
        int i = size;
        if (i >= queue.length)
            grow(i + 1);
        //保证小顶堆结构(在最后一位进行插入)
        siftUp(i, e);
        //实际容量加一
        size = i + 1;
        return true;
    }

PriorityQueue队列移除操作

  通常使用poll()方法移除数据对象,poll()方法具备队列的操作特点,从头部移除数据对象。代码如下

public E poll() {
        final Object[] es;
        final E result;
        //如果条件成立,说明0号索引位置存在数据对象,可以进行移除
        //将result和es的赋值也完成了
        if ((result = (E) ((es = queue)[0])) != null) {
            modCount++;
            final int n;
            //取得当前数组上的最后一个元素,同时完成了容量减一,和n的赋值
            final E x = (E) es[(n = --size)];
            //将最后一个元素置空
            es[n] = null;
            if (n > 0) {
            //和前面的是一个道理
                final Comparator<? super E> cmp;
                if ((cmp = comparator) == null)
                    siftDownComparable(0, x, es, n);
                else
                    siftDownUsingComparator(0, x, es, n, cmp);
            }
        }
        return result;
    }

最后我们想一想,如果移除PriorityQueue队列中的任意位置的一个数据,则有可能破坏堆的结构,我们首先进行降序操作,在进行升序操作。

E removeAt(int i) {
        // assert i >= 0 && i < size;
        final Object[] es = queue;
        modCount++;
        int s = --size;
        //如果恰好是最后一个元素,则直接删除
        if (s == i) // removed last element
            es[i] = null;
        else {
        //将最后一个元素赋给moved,并将这个索引为空
            E moved = (E) es[s];
            es[s] = null;
            //先进行降序操作
            siftDown(i, moved);
            if (es[i] == moved) {
                siftUp(i, moved);
                //条件成了则说明无法进行升序操作,此时该位置和权值刚好匹配
                if (es[i] != moved)
                    return moved;
            }
        }
        return null;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值