1. 程式人生 > >【Java原始碼】集合類-優先佇列PriorityQueue

【Java原始碼】集合類-優先佇列PriorityQueue

一、類繼承關係

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

PriorityQueue只實現了AbstractQueue抽象類也就是實現了Queue介面。

二、類屬性

    //預設初始化容量
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
    
    //通過完全二叉樹(complete binary tree)實現的小頂堆
    transient Object[] queue; 

    private int size = 0;
    //比較器
    private final Comparator<? super E> comparator;
    //佇列結構被改變的次數
    transient int modCount = 0; 

根據transient Object[] queue; 的英文註釋:

Priority queue represented as a balanced binary heap: the two children of queue[n] are queue[2*n+1] and queue[2*(n+1)].

我們可以知道PriorityQueue內部是通過完全二叉樹(complete binary tree)實現的小頂堆(注意:這裡我們定義的比較器為越小優先順序越高)實現的。

三、資料結構

優先佇列PriorityQueue內部是通過堆實現的。堆分為大頂堆和小頂堆:

  • 大頂堆:每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆;
  • 小頂堆:每個結點的值都小於或等於其左右孩子結點的值,稱為小頂堆。
  • 堆通過陣列來實現。
  • PriorityQueue內部是一顆完全二叉樹實現的小頂堆。父子節點下表有如下關係:
leftNo = parentNo*2+1

rightNo = parentNo*2+2

parentNo = (nodeNo-1)/2

通過上面的公式可以很輕鬆的根據某個節點計算出父節點和左右孩子節點。

四、常用方法

add()/offer()

其實add()方法內部也是呼叫了offer().下面是offer()的原始碼:

    public boolean offer(E e) {
        //不允許空
        if (e == null)
            throw new NullPointerException();
        //modCount記錄佇列的結構變化次數。
        modCount++;
        int i = size;
        //判斷
        if (i >= queue.length)
            //擴容
            grow(i + 1);
        //size加1
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            //不是第一次新增,調整樹結構
            siftUp(i, e);
        return true;
    }

我們可以知道:

  • add()和offer()是不允許空值的插入。
  • 和List一樣,有fail-fast機制,會有modCount來記錄佇列的結構變化,在迭代和刪除的時候校驗,不通過會報ConcurrentModificationException。
  • 判斷當前元素size大於等於queue陣列的長度,進行擴容。如果queue的大小小於64擴容為原來的兩倍再加2,反之擴容為原來的1.5倍。
    擴容函式原始碼如下:
    private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        //如果queue的大小小於64擴容為原來的兩倍再加2,反之擴容為原來的1.5倍
        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);
    }
  • 加入第一個元素時,queue[0] = e;以後加入元素內部資料結構會進行二叉樹調整,維持最小堆的特性:呼叫siftUp(i, e):
    private void siftUp(int k, E x) {
        //比較器非空情況
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }
    //比較器非空情況
    @SuppressWarnings("unchecked")
    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            //利用堆的特性:parentNo = (nodeNo-1)/2
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (comparator.compare(x, (E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = x;
    }

上面的原始碼中可知:

  1. 利用堆的特點:parentNo = (nodeNo-1)/2 計算出父節點的下標,由此得到父節點:queue[parent];
  2. 如果插入的元素x大於父節點e那麼迴圈退出,不做結構調整,x就插入在佇列尾:queue[k] = x;
  3. 否則 queue[k] = e; k = parent; 父節點和加入的位置元素互換,如此迴圈,以維持最小堆。

下面是畫的是向一個優先佇列中新增(offer)一個元素過程的草圖,以便理解:

poll(),獲取並刪除佇列第一個元素

    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;
    }

刪除元素,也得調整結構以維持優先佇列內部資料結構為:堆

五、簡單用法

下面是一段簡單的事列程式碼,演示了自然排序和自定義排序的情況:

package com.good.good.study.queue;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Random;

/**
 * @author monkjavaer
 * @version V1.0
 * @date 2019/6/16 0016 10:22
 */
public class PriorityQueueTest {

    /**日誌記錄*/
    private static final Logger logger = LoggerFactory.getLogger(PriorityQueueTest.class);

    public static void main(String[] args) {
        naturalOrdering();
        personOrdering();
    }

    /**
     * 自然排序
     */
    private static void naturalOrdering(){
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        Random random = new Random();
        int size = 10;
        for (int i =0;i<size;i++){
            priorityQueue.add(random.nextInt(100));
        }
        for (int i =0;i<size;i++){
            logger.info("第 {} 次取出元素:{}",i,priorityQueue.poll());
        }
    }

    /**
     * 自定義排序規則,根據人的年齡排序
     */
    private static void personOrdering(){
        PriorityQueue<Person> priorityQueue = new PriorityQueue<>(new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge()-o2.getAge();
            }
        });
        Random random = new Random();
        int size = 10;
        for (int i =0;i<size;i++){
            priorityQueue.add(new Person(random.nextInt(100)));
        }
        while (true){
            Person person = priorityQueue.poll();
            if (person == null){
                break;
            }
            logger.info("取出Person:{}",person.getAge());
        }
    }
}

六、應用場景

優先佇列PriorityQueue常用於排程系統優先處理VIP使用者的請求。多執行緒環境下我們需要使用PriorityBlockingQueue來處理此種問題,以及需要考慮佇列資料持久化