优先队列不是绝对标准的队列实现,每次出队的元素都是优先级最高的元素。不允许添加null元素。
优先队列的元素有两种排序方式:自然排序和定制排序。
自然排序:优先队列集合中元素实现的Comparable接口。
定制排序:创建队列时传进的Comparator对象。
采用了小顶堆的实现方式,保证了每次poll都是最小的元素(也可根据自定义的比较器)。
1.成员变量:
2.构造器:未传入数组的初始化长度则默认长度为11。未传入比较器则默认为null,前提是元素类必须实现了Comparable接口。
//其实上边所说的几种构造器最终会走到该构造器 public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) { //对传入的参数进行判断,小于1则抛出异常 if (initialCapacity < 1) throw new IllegalArgumentException(); //初始化数组 this.queue = new Object[initialCapacity]; this.comparator = comparator; }
传入的是集合:在代码中我们可以看到首先判断集合的类型是否为SortedSet或PriorityQueue,因为这两个类元素类实现了Comparable接口或者定义的时候传进了Comparator对象。所以比较器一定不为空
public PriorityQueue(Collection<? extends E> c) { if (c instanceof SortedSet<?>) { SortedSet<? extends E> ss = (SortedSet<? extends E>) c; this.comparator = (Comparator<? super E>) ss.comparator(); initElementsFromCollection(ss); } else if (c instanceof PriorityQueue<?>) { java.util.PriorityQueue<? extends E> pq = (java.util.PriorityQueue<? extends E>) c; this.comparator = (Comparator<? super E>) pq.comparator(); initFromPriorityQueue(pq); } else { this.comparator = null; initFromCollection(c); } } private void initElementsFromCollection(Collection<? extends E> c) { //将集合转为数组 Object[] es = c.toArray(); int len = es.length; //若c的运行时类型不为Object,则将该数组转为Object类型 if (es.getClass() != Object[].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的长度是否大于0,是就返回该数组,不是则new一个长度为1的Object数组, this.queue = ensureNonEmpty(es); this.size = len; } private void initFromPriorityQueue(PriorityQueue<? extends E> c) { //判断集合类型是否为PriorityQueue,不是则转到initFromCollection方法 if (c.getClass() == PriorityQueue.class) { this.queue = ensureNonEmpty(c.toArray()); this.size = c.size(); } else { initFromCollection(c); } } //此处我们先大概知道heapify是将数组转化成我们想要的堆。 //通过initElementsFromCollection方法已经将集合转为数组了,为什么还要转成我们想要的堆呢? //因为在Sorted集合中数组已经是按我们想要的顺序进行排列,而我们传入的该集合并没有可以让其有顺序的排列的功能,所以还需要调用heapify方法将数组元素的顺序做进一步的转化。 private void initFromCollection(Collection<? extends E> c) { initElementsFromCollection(c); heapify(); }
总结一下:如果没有传入比较器,则元素类必须实现了Comparable接口或者在创建队列时传入Comparator对象。如果没有传入队列的长度就使用源码中的默认初始化长度:11。如果传入的是集合:若集合的类型为SortedSet:转为Object类型的数组;是PriorityQueue则直接转化为数组,因为其本身就是Object类型的数组,无需再做转化;以上两种都不是则先将其转化为Object类型的数组,再调用heapify()进行排序。
3.入队
优先队列内部是以堆来实现的,那我们首先来看看堆是如何添加元素的。(以小顶堆为例)
假如有这样一个数组:1,3,9,8,10 接下来将模拟该数组以堆的方式建立
假如想将2添加进去,第一步就是将2先放到数组的最后一位,如图所示
此时2在数组中的下标为5,通过5我们可以计算出其父节点的下标为(5-1)/2 = 2.可以看到2下标对应的数是9。比较后发现子节点的数值小于父节点,不符合小顶堆的定义,所以更换两个数的位置。如图:
继续比较其与其父节点的大小,2<1,符合小顶堆的定义。至此就添加成功了。接下来将对照源码说明。
public boolean add(E e) { return offer(e);//可以看出add()其实也是调用了offer()完成入队操作。 } public boolean offer(E e) { //如果传进来的是null值就抛异常 if (e == null) throw new NullPointerException(); modCount++;//队列操作数+1. int i = size; //如果此时队列已经满了,则调用grow()进行扩容,该方法待会详说 if (i >= queue.length) grow(i + 1); //优先队列并不是按元素添加的顺序排列的,所以不能直接添加到数组的尾部,调用该方法相当于完成刚才添加元素2的操作。 siftUp(i, e); //更新队列长度 size = i + 1; return true; } private void siftUp(int k, E x) { //如果比较器不为空,使用自定义的比较器进行元素的添加 if (comparator != null) siftUpUsingComparator(k, x, queue, comparator); else //比较器为空。其实两者原理相同,我们详说比较器为空的方法。 siftUpComparable(k, x, queue); } //对参数加以说明:k是当前队列的长度(也是新元素添加进来的下标),x是待入队的元素,es是当前队列 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]; //如果待添加元素的值大于父节点的值,则说明我们找到了新元素待插入的位置,即k,并跳出循环。 if (key.compareTo((T) e) >= 0) break; //如果不是,则将父节点的元素下移,且父节点的下标为待添加元素上移的下标,继续while循环判断当前下标与其父节点的大小。 es[k] = e; k = parent; } //k即为即为我们确定的新节点待插入的位置 es[k] = key; }
其实源码与我们刚才图所示的有点出入,我们发现源码并没有做值得交换,只是一直与其现在所处位置的父节点进行比较,并在满足条件时进行上移,直到找到待插入的位置才将key插入。
上述代码中还有一个方法未提到,grow()。接下来就谈谈优先队列的扩容机制:
private void grow(int minCapacity) { int oldCapacity = queue.length; //可以看到,在调用newLength()这个方法的第三个参数:以旧容量与64的大小做了一个判断:如果旧容量小于64,那么增长因子为2;
若旧容量大于64,那么增长因子就为1.5。
还有一个要说明的是。若传入的minCapacity大于旧容量的2倍,则扩容后的数组的长度为minCapacity。 int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ oldCapacity < 64 ? oldCapacity + 2 : oldCapacity >> 1 /* preferred growth */); queue = Arrays.copyOf(queue, newCapacity); } public static int newLength(int oldLength, int minGrowth, int prefGrowth) { //我们以oldLength=11,minGrowth=1,prefGrowth=13为例 int newLength = Math.max(minGrowth, prefGrowth) + oldLength;//计算得出newLength = 24;即验证了我们刚才所说的增长因子为2:12*2=24。 if (newLength - MAX_ARRAY_LENGTH <= 0) { return newLength; } //当扩容后数组的长度大于MAX_ARRAY_LENGTH则调用hugeLength() return hugeLength(oldLength, minGrowth); }
出队列:
若队列长度为1:返回队顶元素并将队列长度置为0即可。
若队列长度 > 1:队顶元素依然是出队列的元素。不同的是要改变队列元素的位置。
那么问题就来了:如何改变队列元素的位置使其依然是小顶堆呢?
源代码中通过有无比较器调用相对应的方法,但其本质是相同的。
public E poll() { final Object[] es; final E result; //判断队列是否为空 if ((result = (E) ((es = queue)[0])) != null) { modCount++; //队列操作数+1 final int n; //n是当前队列最后一个元素的下标,对应的x就是最后一个元素 final E x = (E) es[(n = --size)]; es[n] = null; 如果n=0:该队列仅有一个元素:x。返回x即可。 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; }
用图来解释 siftDownComparable()该方法是如何工作的:
要做的工作是:元素 1 出队列,并将队列的长度-1。
保存队列的最后一个元素 x = 10; n = 5:为队列最后一个元素的下标,half = n/2 = 2:为去除下标对应的元素后第一个叶子节点的下标 。
找出队顶元素子节点的最小值:2。并保存其下标:child = (k * 2)+1 / (k * 2)+2 ,这个表达式主要是找到k的子节点的最小值。判断最小值2与10的大小:2 < 10,所以2上浮。若k的子节点都比 10 大,说明k就是10待插入的下标。不用再作比较,跳出循环即可。
上述工作完成后另 k = child,比较 k 与 child的大小:若k大则说明k节点下还有子节点可以用来与 10 做比较,不是则说明 k 就是 10待插入的下标。此处 k = 2 , child = 2:说明找到了1带插入的位置,即k。
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 //half是第一个无子节点的节点的下标 while (k < half) { int child = (k << 1) + 1; // assume left child is least Object c = es[child]; int right = child + 1; //找出节点k的子节点中较小的那一个,并将其下标赋给child if (right < n && ((Comparable<? super T>) c).compareTo((T) es[right]) > 0) c = es[child = right]; //若key比较小的那一个还小,则k就是key待插入的下标,并跳出循环 if (key.compareTo((T) c) <= 0) break; //否则将c上浮,并更新待比较节点的位置,直至跳出循环 es[k] = c; k = child; } es[k] = key; }
以上就是我对优先队列源码的分析,不正之处欢迎大家指正。感谢!