Queue队列一般是先进先出(FIFO),但也有特殊的非FIFO,如本文也分析的PriorityQueue。java
Queue接口定义的方法:数组
添加元素接口:安全
删除元素接口:数据结构
获取队列头元素接口:多线程
上图中列出的是Queue平时经常使用的实现类:并发
PriorityQueue是基于二叉堆形式实现的无界队列。队列中元素类型必须是可比较的,构造函数若是没有传入Comparator默认是天然排序。函数
PriorityQueue继承了AbstractQueue,AbstractQueue实现Queue接口,即PriorityQueue拥有Queue的方法和特征。oop
Object[] queue:存放队列元素。线程
int DEFAULT_INITIAL_CAPACITY:默认的队列大小,默认值为11。code
int size:PriorityQueue队列中元素个数。
int modCount:PriorityQueue队列修改次数。
Comparator<? super E> comparator:队列元素排序比较器。
int MAX_ARRAY_SIZE:队列最大值(Integer.MAX_VALUE - 8),VM的保留了8字节的 header words。
package com.juc.queue; import java.util.PriorityQueue; /** * Created on 2020/5/10 23:29. * @author Griez */ public class PriorityQueueTest { public static final PriorityQueue<Integer> QUEUE = new PriorityQueue<>(); public static void main(String[] args) { for (int i = 10; i > 0 ; i--) { QUEUE.offer(i); } for (int i = 0; i < 10; i++) { System.out.println(QUEUE.poll()); } } }
建立一个存放Integer的PriorityQueue,采用默认的天然排序。并倒序的往PriorityQueue添加10-1。而后从PriorityQueue头部出队列并输出,输出结果是1-10升序。若是是让咱们实现应该是入队时用插叙排序好并存放在queue数组中,可是这样实现往queue数组中添加和删除元素移动次数是否是最优的呢?接下来咱们看一下Josh Bloch, Doug Lea是怎么样实现的。
public boolean offer(E e) { if (e == null) //《1》不能为空 throw new NullPointerException(); modCount++; // 《2》修改次数加1 int i = size; if (i >= queue.length) // 默认11 grow(i + 1); // 《3》数组扩容 size = i + 1; if (i == 0) // 《4》直接把e赋值给0下标元素(顶部元素) queue[0] = e; else siftUp(i, e); // 《5》筛选顶部元素 return true; }
《1》添加的元素不能为空,即PriorityQueue队列不可能存在null元素。
《2》修改次数加1。
《3》若是当前PriorityQueue元素数量大于等于数组容量须要对queue进行扩容操做。
《4》若是当前PriorityQueue为空,直接把e赋值给queue数组0下标(顶部元素)。
《5》经过二叉堆,筛选顶部元素。
private void grow(int minCapacity) { int oldCapacity = queue.length; // Double size if small; else grow by 50% // 《1》根据现有的容量选择增加倍数 int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)); // overflow-conscious code // 《2》若是《1》计算出的容量比最大大,则以传入容量为准 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); }
《1》根据现有的容量选择增加倍数,若是如今的容量小于64,则容量直接增加一倍再加2;不然增加50%。
《2》若是《1》计算出的容量比最大大,则以传入容量为准。
private void siftUp(int k, E x) { if (comparator != null) siftUpUsingComparator(k, x); else siftUpComparable(k, x); }
若是构造PriorityQueue时传有特定比较器,就按特定比较器方式设置顶部元素,不然按默认天然比较器方式设置。
private void siftUpComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>) x; //《1》 while (k > 0) { int parent = (k - 1) >>> 1; //《2》 Object e = queue[parent]; //《3》 if (key.compareTo((E) e) >= 0) //《4》 break; queue[k] = e; //《5》 k = parent; } queue[k] = key; //《6》 }
《1》添加的元素必须是Comparable子类,可比较的。
《2》计算父节点下标。
《3》获得父节点元素。
《4》跟父节点元素做比较,若是要添加的元素大于父节点元素则退出。
《5》把父节点的元素移动到数组下标k处,而后把父节点下标赋值给k,循环《1》 - 《4》步骤。
《6》通过前面步骤最终确认须要添加的元素在queue下标,并存入数组。
添加10 - 8 该方法体现的数据结构。
添加7整个过程,用堆数据结构添加7的过程只交换了两次数据位置。若是用插叙排序这种极端状况全部数据都须要移动。
最小二叉堆特性是根节点元素值永远是最小的。
public E poll() { if (size == 0) //《1》 return null; int s = --size; //《2》 modCount++; //《3》 E result = (E) queue[0];//《4》 E x = (E) queue[s];//《5》 queue[s] = null; if (s != 0) siftDown(0, x);//《6》 return result; }
《1》若是队列为空,返回null。
《2》队列元素总数减1。
《3》修改次数加1。
《4》把堆头部元素取出,后面直接返回该元素。
《5》获取queue最后一个元素并把该位置设置null。
《6》从新筛选最小值为头部元素。
private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); }
若是构造PriorityQueue时传有特定比较器,就按特定比较器方式设置顶部元素,不然按默认天然比较器方式设置。
private void siftDownComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>)x; int half = size >>> 1; //《1》 // loop while a non-leaf while (k < half) { int child = (k << 1) + 1; //《2》 // assume left child is least Object c = queue[child];//《3》 int right = child + 1;//《4》 if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) //《5》 c = queue[child = right]; if (key.compareTo((E) c) <= 0)//《6》 break; queue[k] = c;//《7》 k = child; } queue[k] = key;//《8》 }
《1》无符号右移1位,取size的一半。
《2》获得二叉堆的左子节点下标。
《3》获取左子节点元素。
《4》右子节点下标。
《5》右子节点下标小于队列元素总数,而且左子节点元素比右子节点元素大时,把右子节点元素赋值给c,把右子节点下标赋值给child。
《6》须要交换的元素key小于或等于子节点元素c,则退出循环。
《7》把子节点c设置到queue下标为k的位置,并把child赋值给k,而后重复《1》-《6》步骤。
《8》找到key合适的位置并设置该元素。
PriorityQueue使用二叉堆数据结构保证了队列头部元素永远是最小的,在添加和删除的过程元素移动次数比插叙排序插入少。队列元素是使用数组queue保存,在多线程的状况对数组queue并发操做存在安全问题。