jdk原始碼分析之PriorityQueue

王世暉發表於2016-06-07

基本原理

PriorityQueue(優先順序佇列)的資料結構是最小堆,採用陣列作為底層資料結構。
不同於普通的遵循FIFO規則的佇列,PriorityQueue每次都選出優先順序最高的元素出隊,優先佇列裡實際是維護最小堆,通過最小堆使得每次取出的元素總是優先順序最高的。

    /**
     * Priority queue represented as a balanced binary heap: the two
     * children of queue[n] are queue[2*n+1] and queue[2*(n+1)].  The
     * priority queue is ordered by comparator, or by the elements'
     * natural ordering, if comparator is null: For each node n in the
     * heap and each descendant d of n, n <= d.  The element with the
     * lowest value is in queue[0], assuming the queue is nonempty.
     */
    transient Object[] queue; // non-private to simplify nested class access

底層採用Object陣列作為最小堆的實現方式
節點queue[n]的左孩子節點為queue[2*n+1] ,右孩子節點為queue[2*(n+1)]。
queue[0]表示優先順序最高的節點

新增資料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;
    }

首先入口引數檢查,PriorityQueue不支援儲存null,所以如果給PriorityQueue新增null,丟擲空指標異常,在錯誤發生後儘快檢測出錯誤,符合Effective Java第38條原則《檢查引數的有效性》
接著修改modCount,方便在迭代的過程中發生結構性修改可以丟擲ConcurrentModificationException異常
因為是新增資料,所以需要判斷是不是需要擴容

 if (i >= queue.length)
            grow(i + 1);

如果原佇列為空,就把新新增的資料新增到隊首

        if (i == 0)
            queue[0] = e;

否則就呼叫siftUp進行向上篩選

        else
            siftUp(i, e);

篩選分兩種情況,因為有兩種排序規則,按照資料自身的排序規則呼叫siftUpComparable或者按照外部規定的排序規則呼叫siftUpUsingComparator
siftUp(int k, E x)的含義為:在堆的陣列下標k處,放置了一個資料x,此操作有可能破壞堆的性質,因此對堆進行調整

    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

siftUpComparable和siftUpUsingComparator程式碼邏輯類似,只是比較規則不同,因此只取其一進行分析

    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

首先將需要插入的資料x強制轉換為可比較的物件Comparable並儲存為key
然後開始向上篩選,篩選終止的條件有兩個,第一個是待插入的資料比堆頂元素都小,因此會篩選到下標k==0,此時結束迴圈;第二個是在向上篩選的過程中找到一個父節點比待插入節點小,此時不需要繼續向上篩選了,break退出迭代。
迭代的過程中,首先獲取父節點在陣列中的下標位置並將父節點的資料儲存為e

            int parent = (k - 1) >>> 1;
            Object e = queue[parent];

然後判斷如果待插入節點比父節點大,break退出迴圈,待插入節點找到了自己的位置

            if (key.compareTo((E) e) >= 0)
                break;

否則的話,將父節點的資料(陣列下標(k - 1) >>> 1)儲存到正在篩選的節點(陣列下標k),正在篩選的節點的下標k設為父節點下標(k - 1) >>> 1,進行下一次迭代

            queue[k] = e;
            k = parent;

迭代結束後,將待插入資料儲存到在堆中合適的位置

        queue[k] = key;

獲取並刪除隊首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;
    }

首先檢查佇列容量,size==0表示佇列此時沒有資料,從沒有資料的佇列中讀取資料直接返回null
然後對size和modCount進行修改,儲存陣列最後一個元素下標為s=size-1

        int s = --size;
        modCount++;

最小堆的性質保證優先順序佇列的隊首元素queue[0]一定是最小的(優先順序最高的),因此將隊首元素儲存為result

        E result = (E) queue[0];

隊首元素出佇列後,將隊尾元素儲存到隊首,隊尾元素置null加速垃圾回收,這個操作有可能破壞了堆的性質,因此需要向下篩選

        E x = (E) queue[s];
        queue[s] = null;
        if (s != 0)
            siftDown(0, x);

siftDown(int k, E x)的含義為:將優先順序佇列陣列下標為k處的資料設定為x,此操作有可能破壞了堆的性質,因此需要調整。

    private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

和向上篩選一樣,分兩種排序規則進行分類處理,但是兩種情況程式碼大同小異,只是比較規則不一樣,選其一進行分析

    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = x;
    }

首先找到葉子節點的最小下標,也就是第一個葉子節點的下標,並儲存為half。

        int half = size >>> 1;

最小堆也是一個完全二叉樹,完全二叉樹的性質決定了size的一半減一(size/2-1)是非葉子節點的最大下標,size的一半(size/2)是葉子節點的最小下標
然後向下篩選,要麼找到了比篩選到了葉子節點,要麼找到了資料x在堆中的合適位置,這兩種情況都停止篩選
篩選的過程中,需要將待篩選節點和兩個孩子節點進行比較,如果待篩選節點比兩個孩子都小,則向下篩選結束,否則交換較小孩子和待篩選節點的位置,進行下一輪篩選。

            int child = (k << 1) + 1;
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];

然後左右孩子比較,設定左右孩子的較小者為c,然後用c和待篩選節點x進行比較

            if (comparator.compare(x, (E) c) <= 0)
                break;

如果待篩選節點x比左右孩子的較小者c都小,說明待篩選節點x找到了自己的合適位置,停止篩選,break退出迴圈
否則進行下一輪篩選

            queue[k] = c;
            k = child;

將左右孩子較小者c儲存到父節點queue[k] ,設定下一輪待篩選節點下標k為左右孩子較小者下標,進行新的一輪篩選
篩選過程結束,將資料x放置到自己在最小堆中的最終位置

        queue[k] = x;

建堆過程

    private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }

建堆的過程只從非葉子節點開始篩選,因為篩選的過程中會處理到葉子節點。並且篩選的過程是倒著來的,從最後一個非葉子節點開始,一直處理到陣列中第一個節點,這種自底向上的思想類似於動態規劃。

相關文章