PriorityQueue 源碼分析

PriorityQueue

一個無限的優先級隊列基于一個優先級堆。優先級隊列中的元素根據它們的Comparable自然順序或通過在隊列構造時提供的Comparator來排序。(如果有Comparator就根據Comparator來對元素進行排序,否則根據元素自己的Comparable來進行排序)。一個優先級隊列不允許‘null’元素。一個依賴自然排序的優先級隊列甚至不允許插入一個不可比較(non-comparable)的對象(如果你插入一個non-comparable對象,則會拋出一個ClassCastException異常)。

隊列的頭(head)元素是相對于指定順序的最小的(least)元素。如果多個元素被綁為最小值,那么頭元素是它們中的一個————綁定會被任意的破壞。隊列的檢索操作poll、remove、peek和element都會訪問隊列頭(head)元素。

一個優先級隊列是無限制的,但是它有一個內部的“capacity”管理著數組的大小,該數組用于存儲隊列的元素。它總是至少同隊列大小一樣大。當元素加到優先級隊列中,它的容量會自動增加。并沒有指定增長策略的細節。

該類和它的迭代器實現了Collection和Iterator接口所有可選的方法。迭代器提供的iterator()方法不保證遍歷優先級隊列的元素根據任何特別的順序。如果你需要有序的遍歷,考慮使用Arrays.sort(pq.toArray()).
注意,PriorityQueue類的實現是非同步的。如果任何一個線程修改隊列,多線程不應該同時訪問一個PriorityQueue實例。相反,應該使用線程安全的PriorityBlockingQueue類。

實現注意:該實現提供了O(log(n))時間復雜度對于入隊和出隊方法:offer、poll、remove()和add;線性的時間O(n)對于remove(object)和contains(object)方法;和常量的時間O(1)對于檢索方法:peek、element和size。

屬性

    /**
     * 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

優先級隊列表現為一個平衡二項堆(即,平衡二叉樹):queue[n]的兩個兒子分別是queue[2n+1]和queue[2(n+1)]。優先級隊列通過比較器(comparator)來排序,或者如果比較器為空則通過元素的自然順序來排序:堆中每個節點n和n的每個后裔節點d,n <= d。假設隊列是非空的,那么具有最低值的元素在queue[0]。

優先級隊列的數據結構是一個平衡二叉樹,并且數中所有的子節點必須大于等于父節點,而同一層子節點間無需維護大小關系。這樣的結構性讓優先級隊列看起來像是一個最小堆。

父節點與子節點間的索引關系:
① 假設父節點為queue[n],那么左孩子節點為queue[2n+1],右孩子節點為queue[2(n+1)]。
② 假設孩子節點(無論是左孩子節點還是右孩子節點)為queue[n],n>0。那么父節點為queue[(n-1) >>> 1]

節點間的大小關系:
① 父節點總是小于等于孩子節點
② 同一層孩子節點間的大小無需維護

葉子節點與非葉子節點:
① 一個長度為size的優先級隊列,當index >= size >>> 1時,該節點為葉子節點。否則,為非葉子節點。"附"中會對該結論做個簡單的證明。

    /**
     * 優先級隊列元素的個數
     */
    private int size = 0;

    /**
     * 優先級隊列結構上被修改的次數。修改操作包括:clear()、offer(E)、poll()、removeAt(int)
     */
    transient int modCount = 0; // non-private to simplify nested class access


方法

  • 添加節點
    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;
    }

往優先級隊列中插入元素,如果隊列滿了,則進行擴容。插入操作必要的話是會導致堆元素調整的,以滿足父節點總是小于等于子節點的要求。
插入操作的時間復雜度為O(log(n));

通過siftUp方法來完成元素插入時的調整:siftUp(index, object)方法會升高待插入元素在樹中的位置index,直到待插入的元素大于或等于它待插入位置的父節點

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

    @SuppressWarnings("unchecked")
    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;
    }

    @SuppressWarnings("unchecked")
    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            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;
    }

通過“int parent = (k - 1) >>> 1;”獲取到當前要插入節點位置的父節點,比較父節點和待插入節點,如果待插入節點小于父節點,則將父節點插入到子節點的位置,然后在獲取父節點的父節點循環上面的操作,直到待插入節點大于等于父節點,則在相應位置插入這個節點。最終保證代表優先級隊列的平衡二叉樹中,所有的子節點都大于它們的父節點,但同一層的子節點間并不需要維護大小關系。

圖解“添加節點”步驟:
往一個空的優先級隊列中依次插入“13”、“-3”、“20”、“-25”
① 插入“13”


② 插入“-3”

③ 插入“20”

④ 插入“-25”


  • 獲取優先級隊列頭結點
    public E peek() {
        return (size == 0) ? null : (E) queue[0];
    }

獲取優先級隊列頭元素,也就是優先級隊列中值最小的元素。
獲取操作的時間復雜度為O(1)

  • 刪除節點
    public boolean remove(Object o) {
        int i = indexOf(o);
        if (i == -1)
            return false;
        else {
            removeAt(i);
            return true;
        }
    }

該刪除操作的最壞耗時為:n + 2log(n); 所以該操作的的時間復雜度為O(n);
indexOf(object)操作時間復雜度為O(n);
removeAt(index)操作時間復雜度為O(log(n))

    private E removeAt(int i) {
        // assert i >= 0 && i < size;
        modCount++;
        int s = --size;
        if (s == i) // removed last element
            queue[i] = null;
        else {
            E moved = (E) queue[s];
            queue[s] = null;
            siftDown(i, moved);
            if (queue[i] == moved) {
                siftUp(i, moved);
                if (queue[i] != moved)
                    return moved;
            }
        }
        return null;
    }

如果待刪除節點位置為隊列尾,則直接將隊列尾位置置null。否則將隊列尾節點前插以覆蓋待刪除節點位置的節點。
當待刪除節點的位置為非葉子節點時,會進行一系列的節點調整,使得隊尾節點在前插后能保證優先級隊列數據結構的正確性。
當待刪除節點的位置為葉子節點時,會先將隊尾節點設置到待刪除節點位置以使得隊列中已經沒有待刪除節點了,然后再進行已經插入到新位置的隊尾節點同它新父節點進行比較調整,以保證父節點總是小于等于子節點,即保證優先級隊列數據結構的正確性。
當該方法進行siftUp操作來對節點進行結構調整后使得隊尾節點最終并不是被設置到了待刪除節點位置,這時就返回這個前插的隊尾元素。因為這種情況下,刪除操作會涉及到一個未訪問的元素被移動到了一個已經訪問過的節點位置。在迭代器操作中需要特殊處理。

通過siftDown方法來完成元素移除時的調整:siftDown(index, object)方法會降低待插入元素在樹中的位置index,直到待插入的元素小于或等于它待插入位置的孩子節點。

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

    @SuppressWarnings("unchecked")
    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;
    }

    @SuppressWarnings("unchecked")
    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;
    }

因為在平衡二叉樹中,葉子節點的個數總是大于等于前面所有非葉子節點個數之和。所有如果待刪除元素的所在位置大于等于隊列長度的一半,則說明待刪除的節點是一個葉子節點,則直接將隊列中最后一個節點值(注意,隊列中最后一個節點一定也是葉子節點)設置到待刪除節點所在位置。
如果待刪除節點的位置小于隊列長度的一半,則說明待刪除的節點是一個非葉子節點。那么先取得待刪除節點的子節點中小的那個子節點,將該子節點與隊列中最后一個節點進行比較,如果子節點小于隊列中最后一個節點,則將子節點值設置到待刪除節點的位置,然后再次獲取當前子節點的較小的子節點重復一樣的操作,直到隊列最后一個節點比較小的那個子節點還要小,則將隊列最后一個節點值設置為這個子節點的父節點。最終保證代表優先級隊列的平衡二叉樹中,所有的父節點都小于等于它的子節點,但同一層的子節點間并不需要維護大小關系。

圖解“刪除節點”步驟:
假設有如下優先級隊列:


情況一:刪除“queue[7]=18”

情況二:刪除“queue[2]=-23”



  • 是否包含節點
    public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

判斷優先級隊列中是否包含object對象。該方法的時間復雜度為:O(n)

    private int indexOf(Object o) {
        if (o != null) {
            for (int i = 0; i < size; i++)
                if (o.equals(queue[i]))
                    return i;
        }
        return -1;
    }


  • 移除并獲取優先級隊列頭節點
    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;
    }

移除并獲取優先級隊列頭節點。該操作的時間復雜度為:O(log(n));

  • 清除優先級隊列中所有節點
        modCount++;
        for (int i = 0; i < size; i++)
            queue[i] = null;
        size = 0;
    }

清除優先級隊列中的所有節點。該操作的事件復雜度為:O(n);

  • 迭代器
    優先級隊列的迭代器并不保證遍歷按照指定的順序獲取節點元素。這是因為當在迭代器中執行remove操作時,可能會涉及到一個未訪問的元素被移動到了一個已經訪問過的節點位置(刪除操作時,當隊尾節點被放置到待移除節點位置的情況下,需要調用siftUp方法,siftUp(index, object)方法會升高待插入元素在樹中的位置index,直到待插入的元素大于或等于它待插入位置的父節點)。在迭代器操作中需要特殊處理。此時這些不幸的元素會在所有節點遍歷完后才得以遍歷。


  • 證明“在平衡二叉樹中,葉子節點的個數總是大于等于前面所有非葉子節點個數之和。”
    一個平衡二叉樹第N層節點數為:2^N
    一個N層的平衡二叉樹總節點數為:2^(N+1) -1;
    Sn = 2^0 + 2^1 + …… + 2^N
    2*Sn = 2^1 + 2^2 + …… + 2^N + 2^(N+1)
    將二個等式的左邊和右邊分別進行相減操作得到:
    (2-1)Sn = 2^(N+1) - 2^0 ==> Sn = 2^(N+1) -1;
    所以一個N層的二叉平衡樹除了葉子節點外的節點總數最大為:2^N -1;
    因而2^N > 2^N -1,所以在滿平衡二叉樹下,葉子節點大于非葉子節點個數之和,當然在最后一層節點不滿的情況下,葉子節點依舊大于等于所有非葉子之和。

后記

若文章有任何錯誤,望大家不吝指教:)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內容

  • 1 序 2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,125評論 0 12
  • 四. 走向世界之巔——快速排序 你可能會以為歸并排序是最強的算法了,其實不然。回想一下,歸并的時間效率雖然高,但空...
    Leesper閱讀 1,737評論 9 7
  • 源碼來自jdk1.8 PriorityQueue內部由最小堆實現,也就是說每次執行add或是remove之后,總是...
    言西棗閱讀 997評論 0 0
  • B樹的定義 一棵m階的B樹滿足下列條件: 樹中每個結點至多有m個孩子。 除根結點和葉子結點外,其它每個結點至少有m...
    文檔隨手記閱讀 13,285評論 0 25
  • 經不可以輕傳,也不可輕取。----《西游記》 這是今天學習熊太行《關系攻略》專欄的收獲,受益匪淺! 整理如下: 1...
    暖暖的大樹閱讀 581評論 0 0