1. 程式人生 > >Queue佇列API與原始碼分析優先順序佇列PriorityQueue實現原理

Queue佇列API與原始碼分析優先順序佇列PriorityQueue實現原理

public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)
            grow(i + 1);        //@1  容量擴容
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            siftUp(i, e);       // @2
        return true;
    }
程式碼@1,擴容,比較簡單,下面直接將程式碼copy出來,瀏覽一下即可:
/**
     * Increases the capacity of the array.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        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 int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
重點關注@2,siftUp的實現:
private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

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

    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {                 // @1
            int parent = (k - 1) >>> 1;  //@2
            Object e = queue[parent];
            if (comparator.compare(x, (E) e) >= 0) //@3
                break;
            queue[k] = e;      // @4
            k = parent;         //@5
        }
        queue[k] = x;          //@6
    }
重點分析一下siftUpUsingComparator方法,比較器不為空 首先,引數k的值為增加元素之前的size值,也就是,PriorityQueue總是在先將節點放在內部陣列元素的索引為size的位置,size加一,然後找到 element[size]的父節點,對比兩者之間的大小關係,如果小於父節點,則交換兩者的位置,繼續像上找其父節點。直到父節點為空,索引<=0 程式碼@2,上文中也說過,PriorityQueue用陣列來儲存一棵完成二叉樹,索引為n的左右子節點的索引分別為2n+1,2n+2( 2(n+1)),那已知子節點的索引為k,父節點的索引則為 看 (k -1) / 2 ,所以就不難理解 (k-1)>>>1。parent為父節點的索引。 程式碼@3,如果子節點的值大於父節點的值,由於滿足最小堆的定義(父節點比兩個子節點的值都要小),不需要調整樹的結構,直接將元素x新增到陣列索引代表的k位置。 程式碼@4,如果子節點的值小於父節點的值,則將父節點的值存入到k位置,設定k為parent,如果k的值小於等於0,則執行@6,否則繼續找parent的父節點,再次比較父節點與x的大小。
2.2 public E poll() 方法詳解
public E poll() {
        if (size == 0)
            return null;
        int s = --size;
        modCount++;
        E result = (E) queue[0];       // @1
        E x = (E) queue[s];               // @2
        queue[s] = null;                 //@3          
        if (s != 0)
            siftDown(0, x);                
        return result;                      
    }
再重複一下poll方法的語義,如果佇列為空,則返回null,否則移除佇列中的第一個元素,並返回。 程式碼@1,返回佇列中第一個元素,最小堆中,queue[0]代表根節點。 程式碼@2,獲取陣列中最後一個元素,該陣列有個特點,如果索引size-1的元素肯定不為空,並且為該陣列最後一個不為空的元素。陣列中的元素是連續不為空,因為從演算法角度來說,在新增元素的時候,總是首先在 queue[size-1]的位置新增元素,然後與該位置的父節點去比較,並且總是先新增左節點,然後新增右節點。從而保證陣列元素是連續的;從完成二叉樹的定義中也規定,只有最後兩層的節點的度少於二,也保證了陣列元素不會出現不連續,所謂的不連續是不允許 queue[0],queue[2]不為空,但queue[1]為空的情況。 程式碼@3,將queue最後一個節點設定為空,然後用該節點與根節點去做比較,將該樹中最小值設定為新的根節點,達到將舊根節點移除的目的。具體請看如下程式碼。
/**
     * Inserts item x at position k, maintaining heap invariant by
     * demoting x down the tree repeatedly until it is less than or
     * equal to its children or is a leaf.
     *
     * @param k the position to fill
     * @param x the item to insert
     */
    private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

    private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        int half = size >>> 1;        // loop while a non-leaf
        while (k < half) {
            int child = (k << 1) + 1; // assume left child is least
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
            if (key.compareTo((E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = key;
    }

    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;                            // @1
        while (k < half) {                                   //@2
            int child = (k << 1) + 1;                   //@3
            Object c = queue[child];                 //@4
            int right = child + 1;                        //@5
            if (right < size &&                             //@6
                comparator.compare((E) c, (E) queue[right]) > 0)    //@7
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0)                       //@8
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = x;                                                                     //@9
    }
程式碼siftDownUsingComparator是從根節點開始重構該樹,使之維持最下堆特性。入參中的k為0,x為原先最後一個元素。該方法的目的是要移除頭部節點,然後找到最新的頭節點。實現方法是,用指定的位置k節點開始下沉,找到最小節點成為新的頭部節點,如果k節點沒有子節點,直接將位置k的節點設定為x,原k節點的資料被刪除;如果k有子節點,然後找到k的兩個子節點中最小的那個節點位置(child為最小的節點),如果K的最小的子節點比x的大的話,說明x符合成為K兩個子節點的父節點,故直接將x替換原來的k即可,否則將最小的子節點替換K,然後從k=child,重複上述操作。 程式碼@1,因為最後一個元素為size-1,根據該陣列的特性,0-size/2的節點有子節點,不包括size/2,[0,half)為所有的根節點。故程式碼@2的迴圈條件為k>half, 程式碼@3,k的左孩子的索引 程式碼@4,k的左孩子節點的值 程式碼@5,k的右孩子 程式碼@5,@7,如果右孩子不為空,則取,左右兩個孩子中比較小的值,用來比較。 程式碼@7,如果x的值比待替換的根節點的兩個孩子節點都小,直接將X替換為根節點即可,執行程式碼@9。 否則,用根節點下兩個子節點中最小值替換一下根節點。然後從child位置重複上面的計算,確保新的子數也符合最小堆的定義,while迴圈結束有如下兩種情況,1:k>=half,說明位置k已經是葉子節點了,故需要跳出執行;如果x的值比父節點的兩個子節點都小,跳出迴圈。將下標k處的值設定為x。
2.3 public boolean remove(Object o)
public boolean remove(Object o) {
        int i = indexOf(o);       
        if (i == -1)
            return false;
        else {
            removeAt(i);   // @1
            return true;
        }
    }
private E removeAt(int i) {
        assert i >= 0 && i < size;
        modCount++;
        int s = --size;
        if (s == i) // removed last element
            queue[i] = null;
        else {                                          // @2
            E moved = (E) queue[s];       //@3
            queue[s] = null;                    //@4
            siftDown(i, moved);              //@5
            if (queue[i] == moved) {      //@6
                siftUp(i, moved);             //@7
                if (queue[i] != moved)
                    return moved;
            }
        }
        return null;
    }
該方法的實現思想是首先在queue陣列中找到該值,如果找不到,直接返回-1,是佇列最後一個元素,直接從佇列中刪除即可,如果是佇列中間的元素,刪除操作就沒那麼簡單,刪除元素後,要保證陣列元素的連續性(其實就是要保證刪除後的樹滿足最小堆的定義) 程式碼@3、@4,先用臨時變數moved儲存佇列的最後一個元素,然後將佇列最後一個元素刪除(設定為空) 程式碼@5,刪除下標為i的元素,然後用moved元素來填充一個空位。    siftDown執行完畢後,moved元素所在的位置有如下幾種可能:    1、moved放在下標為i的地方,這裡包含兩種情況,1)位置為i的元素為葉子節點。2)如果move元素比i的兩個         子元素小。    2、moved放在下標大於i的地方,如果moved元素比i的最小的子節點大,此時用最小的子節點替換父節點。i的           索引設定為原i最小節點的索引,從該索引繼續執行siftDown過程。    如果moved放在下標大於i的地方,本次刪除成功結束。    如果moved位置放在i的位置,此時,需要上浮,確定moved是否比父節點大,如果比父節點小,則不符合最小      堆定義,通過上浮調整。siftUp方法不會刪除元素,但siftDown方法會刪除一個元素。 接下來,就是從待移除的位置 i 處開始下沉,我們知道 siftDown(int k,E x)方法,可以說就是移除k位置的元素,然後將加入到以k為父節點的樹中(保持最小堆特性),但要是位置為k的地方是葉子節點呢?siftDown的做法是直接將x放入到k的位置。但這樣會不會影響最小堆特性呢,答案是可能的,也就有了程式碼@6的判斷,如果queue[i] == moved,說明此時k是葉子節點,,應該從索引為k向上升,判斷該節點與父節點的關係,所以呼叫siftUp(int i, E x)方法,上浮,確保維持最小堆特性。在這裡,我還想再囉嗦一下,重溫一下siftUp的實現邏輯:如果i<=0,直接將x放入到佇列索引為i的位置。如何大於0,則找到該節點的父節點,然後比較父節點與x的大小關係,如果x大於根節點,則直接將x放入到下標為i的地方,否則,需要將父節點放入位置i上,然後繼續k=parent。 提問:     PriorityQueue removeAt 返回值,是被移除的元素嗎? 2.4 public Iterator<E> iterator()    迭代器 首先,迭代器,既然PriorityQueue內部是一個數組queue[],那迭代器,直接遍歷該陣列就好了,是的,但迭代器與for(int i =0; i < size; i++)這中遍歷方法,還有一個特殊的是,迭代器支援將當前迭代的元素刪除,也就是remove方法。我們也知道,移除一個元素後,要重新調整結構,保證刪除一元素後繼續保持最小堆的特性。從上文中我們知道,從0-size-1直接刪除一個元素,通常的做法,是先將尾部節點(移除佇列,將size減一,然後將queue[size]的元素用臨時變數儲存,然後將queue[size]=null),用該節點與要刪除的元素進行對比,替換,再重構樹特性。 所以,Itr類的設計,就是基於上述的考慮。
private final class Itr implements Iterator<E> {
        /**
         * Index (into queue array) of element to be returned by
         * subsequent call to next.
         */
        private int cursor = 0;      //@1

        /**
         * Index of element returned by most recent call to next,
         * unless that element came from the forgetMeNot list.
         * Set to -1 if element is deleted by a call to remove.
         */
        private int lastRet = -1;   //@2

        /**
         * A queue of elements that were moved from the unvisited portion of
         * the heap into the visited portion as a result of "unlucky" element
         * removals during the iteration.  (Unlucky element removals are those
         * that require a siftup instead of a siftdown.)  We must visit all of
         * the elements in this list to complete the iteration.  We do this
         * after we've completed the "normal" iteration.
         *
         * We expect that most iterations, even those involving removals,
         * will not need to store elements in this field.
         */
        private ArrayDeque<E> forgetMeNot = null;    //@3

        /**
         * Element returned by the most recent call to next iff that
         * element was drawn from the forgetMeNot list.
         */
        private E lastRetElt = null;                                //@4

        /**
         * The modCount value that the iterator believes that the backing
         * Queue should have.  If this expectation is violated, the iterator
         * has detected concurrent modification.
         */
        private int expectedModCount = modCount;

        public boolean hasNext() {       //@5
            return cursor < size ||
                (forgetMeNot != null && !forgetMeNot.isEmpty());
        }

        public E next() {
            if (expectedModCount != modCount)
                throw new ConcurrentModificationException();
            if (cursor < size)
                return (E) queue[lastRet = cursor++];
            if (forgetMeNot != null) {
                lastRet = -1;
                lastRetElt = forgetMeNot.poll();
                if (lastRetElt != null)
                    return lastRetElt;
            }
            throw new NoSuchElementException();
        }

        public void remove() {
            if (expectedModCount != modCount)
                throw new ConcurrentModificationException();
            if (lastRet != -1) {
                E moved = PriorityQueue.this.removeAt(lastRet);
                lastRet = -1;
                if (moved == null)
                    cursor--;
                else {
                    if (forgetMeNot == null)
                        forgetMeNot = new ArrayDeque<>();
                    forgetMeNot.add(moved);
                }
            } else if (lastRetElt != null) {
                PriorityQueue.this.removeEq(lastRetElt);
                lastRetElt = null;
            } else {
                throw new IllegalStateException();
            }
            expectedModCount = modCount;
        }
    }
程式碼@1,cursor 遍歷陣列的遊標,從0開始, 從這裡也可以看出,在沒有呼叫it.remove方法時,就是遍歷整個陣列元素。 程式碼@2,lastRet 上一次返回的元素索引。-1代表未返回。 程式碼@3,forgetMeNot 這個佇列由什麼用呢?大家可以先想想。 程式碼@4,lastRetElt 上一次返回的元素。 程式碼@5,判斷是否還有可遍歷的元素,如果cursor 小於size,或者forgetMeNot 不為空,說明有元素可遍歷,這裡forgetMeNot不為空,為什麼就會有元素可遍歷呢?其實將目光放入到remove方法時,會發現,forgetMeNot中的元素,其實就是removeAt返回的元素,那我們來探究removeAt(int i)在什麼情況下會返回不為空的元素,PriorityQueue在移除下標為i的元素,是這樣處理的,首先,將佇列的size-1,然後將最後一個元素出佇列,記為moved,然後去執行 siftDown( i , moved),此方法會首先肯定能刪除原先位置為i的元素,但moved會放在佇列的什麼地方呢?因為moved存入的位置,直接會影響到@1中cursor遊標去順序訪問陣列相關。如果moved最後放在大於i的位置,則可以直接返回null,否則需要返回,並放入一個臨時地方,保證最後能遍歷到。所以,forgetMeNot就是用來儲存未遍歷到,但已經移動到小於或等於cursor的位置的元素。     在此,特意感謝 http://shmilyaw-hotmail-com.iteye.com/blog/1775868 該篇文章詳細講解了最小堆的實現原理,讓我受益匪淺。