Java PriorityQueue 中二叉堆原理

問:談談你對二叉堆數據結構的理解及在 PriorityQueue 中的實現?

答:這算是一道比較有深度的問題了,要回答好首先得解釋什么是二叉堆數據結構,接著解釋其優點,然后解釋在 JDK 1.5 的 PriorityQueue 中是怎么使用的,只有這幾個方面都點到才算比較滿意的答案。

首先堆的特點總是一棵完全二叉樹且某個節點值總是不大于或不小于其父節點值,PriorityQueue 使用的是堆中比較特殊的二叉堆,二叉堆是完全二叉樹或者是近似完全二叉樹,二叉堆分為最大堆和最小堆,最大堆的父結點值總是大于或等于任何一個子節點值,最小堆的父結點值總是小于或等于任何一個子節點值。如下圖就是一個最小二叉堆結構圖:

可以看見二叉堆(完全二叉樹)在第 N 層深度被填滿之前是不會開始填第 N+1 層的,且元素插入也是從左往右順序。此外我們通過上面的樹狀圖和數組連續內存分布可以看到父子節點的索引順序存在如下關系:

parentNodeIndex = (currentNodeIndex-1)/2;

leftNodeIndex = parentNoIndex*2+1;

rightNodeIndex = parentNodeIndex*2+2;

可以看見,通過公式能直接計算出某個節點的父節點以及子節點的下標,所以這也就是為什么可以直接用數組來存儲二叉堆而不用鏈表的原因之一,故 PriorityQueue 的 peek()/element() 操作時間復雜度是 O(1),而 add()/offer()/poll()/remove() 操作的時間復雜度是 O(log(N))。

了解了二叉堆的原理和特點之后我們就來看看 PriorityQueue 中是怎么使用二叉堆實現操作的,我們主要要看的方法為add()/offer()/peek()/element()/poll()/remove(),下面會對這些方法進行分組實現解說。

1. add()/offer()

PriorityQueue 的 add()/offer() 操作都是向優先隊列中插入元素,add() 的實現就是直接調用 offer() 方法返回,所以我們直接看下 offer() 方法的實現:

   public boolean offer(E e) {
        //PriorityQueue元素不允許為null
        if (e == null) throw new NullPointerException();
        modCount++;
        int i = size;
        //數組需要擴容,arraycopy操作 
        if (i >= queue.length) grow(i + 1);
        size = i + 1;
        if (i == 0)
            queue[0] = e;
            //隊列為空時第一個元素插在數組開頭 
        else
            siftUp(i, e);
        //隊列不為空時堆結構調整數組元素位置 
        return true;
    }

    // 使用不同的比較器進行比較
    private void siftUp(int k, E x) {
        if (comparator != null) siftUpUsingComparator(k, x);
        else siftUpComparable(k, x);
    }

    //k為currentNodeIndex,x為要插入的元素 
    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            //等價于parentNodeIndex=(currentNodeIndex-1)/2; 
            Object e = queue[parent];
            //將x逐層與parent元素比較交換,只到x>=queue[parent]結束 
            if (key.compareTo((E) e) >= 0) break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

上面的代碼用圖示流程演示如下(9個元素的優先級列表插入一個調整后變為10個元素):

2. peek()/element()

PriorityQueue 的 peek()/element() 操作,都是取出最小堆頂元素但不刪除隊列堆頂元素,區別就是 element() 的實現是 peek() 且 element() 取出元素為 null 會拋出異常而 peek() 不會,所以我們直接看下 peek() 方法的實現:

        public E peek () {
            return (size == 0) ? null : (E) queue[0];
        }

演示流程圖如下:


3. poll()

PriorityQueue 的 poll() 操作,其目的就是取出最小堆頂部元素并從隊列刪除,當失敗時返回 null,所以該方法的實現如下:

    public E poll() {
        if (size == 0) return null;
        int s = --size;
        modCount++;
        //最小二叉堆的最小元素自然在數組的index為0處
        E result = (E) queue[0];
        // 取出數組最后一個元素,即二叉堆樹最深層最右側的元素
        E x = (E) queue[s];
        // 最后一個元素位置置空
        queue[s] = null;
        if (s != 0) siftDown(0, x);
        // 調整二叉堆數組元素位置
        return result;
    }

    // 直接看siftDown中的Comparable情況,k索引0開始,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;
    }

上面的代碼用圖示流程演示如下(10個元素的優先級列表 poll 刪除一個調整后變為9個元素):

4. remove()

PriorityQueue 的 remove(E e) 操作,其目的就是將指定的元素從隊列刪除,當失敗時返回 false,該方法的實質是先遍歷獲取 e 在數組的 index 索引,然后調用 removeAt(index) 方法,所以我們看下 removeAt(index) 方法源碼如下:

        //i為要刪除的元素在數組的索引
        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;
                //然后類似poll進行siftDown向下子節點比較交換(從i位置當做頂層父節點)
                siftDown(i, moved);
                // 向下沉淀完發現沒變化則需要向上浮動,說明最后一個元素換到 i 位置后是最小元素
                if (queue[i] == moved) {
                    siftUp(i, moved);
                    if (queue[i] != moved) return moved;
                }
            }
            return null;
        }

上面的代碼用圖示流程演示如下(10個元素的優先級列表 remove 刪除一個調整后變為9個元素):


在作答這個題時你可以選擇畫圖也可以選擇直接寫父子節點關系公式和 siftUp、siftDown 的機制即可,核心答出來就行,當然不要忘記最小二叉堆是數組實現且 PriorityQueue 元素不允許為空的特性。

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

推薦閱讀更多精彩內容