【java容器的刻意練習(xí)】【十七】PriorityQueue的插入源碼分析

上一篇我們知道了PriorityDeque的底層結(jié)構(gòu),是個(gè)平衡二叉堆,用“兵陣變隊(duì)列”的方式儲(chǔ)存在數(shù)組中。

這一篇我們開始學(xué)習(xí),PriorityDeque是如何利用平衡二叉堆實(shí)現(xiàn)優(yōu)先級(jí)排序的。

先看添加元素的方法:

    public boolean add(E e) {
        return offer(e);
    }

原來(lái)addoffer封裝而已,看看offer源碼:

    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)
            grow(i + 1);
        siftUp(i, e);
        size = i + 1;
        return true;
    }

offer添加元素的邏輯如下:

  • modCount操作次數(shù)加1,默認(rèn)是0
  • size是隊(duì)列長(zhǎng)度,默認(rèn)是0
  • 如果數(shù)組長(zhǎng)度不夠,則需要調(diào)用grow擴(kuò)容
  • 數(shù)組長(zhǎng)度足夠,調(diào)用siftUp進(jìn)行元素添加
  • 隊(duì)列長(zhǎng)度size加1

grow函數(shù)我們比較熟悉了,基本上java里面的自動(dòng)數(shù)組擴(kuò)容都是這個(gè)邏輯:長(zhǎng)度在64以下是擴(kuò)容至原長(zhǎng)度2倍+2;大于64長(zhǎng)度就是每次擴(kuò)容50%。當(dāng)然會(huì)充分考慮一些越界問題。

    private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // Double size if small; else grow by 50%
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity < 64 ? oldCapacity + 2 : oldCapacity >> 1
                                           /* preferred growth */);
        queue = Arrays.copyOf(queue, newCapacity);
    }

接下來(lái),我們重點(diǎn)看之前沒見過(guò)的函數(shù)siftUp(int k, E x)

    /**
     * Inserts item x at position k, maintaining heap invariant by
     * promoting x up the tree until it is greater than or equal to
     * its parent, or is the root.
     *
     * To simplify and speed up coercions and comparisons, the
     * Comparable and Comparator versions are separated into different
     * methods that are otherwise identical. (Similarly for siftDown.)
     *
     * @param k the position to fill
     * @param x the item to insert
     */
    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x, queue, comparator);
        else
            siftUpComparable(k, x, queue);
    }

siftUp的邏輯如下:

  • 如果我們構(gòu)造函數(shù)時(shí)候傳入了自定義的comparator比較器,就用siftUpUsingComparator進(jìn)行優(yōu)先級(jí)判斷加入;
  • 如果沒有自定義比較器,就用默認(rèn)的siftUpComparable函數(shù)進(jìn)行排序后再加入。

注釋提到,siftUp實(shí)現(xiàn)在數(shù)組的k位置插入元素x,通過(guò)“上浮”x直到它大于或等于其父節(jié)點(diǎn),或者x變成根節(jié)點(diǎn),來(lái)保持最小堆的平衡,就是“小頂堆”。為了簡(jiǎn)化和加速比較,默認(rèn)比較器和自定義比較器被分成不同的方法,其他實(shí)現(xiàn)是相同的。(類似siftDown。)

那么,既然注釋siftUpComparablesiftUpUsingComparator就差個(gè)自定義比較而已,那么我們先看默認(rèn)的排序函數(shù)siftUpComparable的實(shí)現(xiàn)邏輯:

  /**
   * 將元素x插到數(shù)組k的位置.
   * 然后按照元素的自然順序進(jìn)行堆調(diào)整——"上浮",以維持"堆"有序.
   * 最終的結(jié)果是一個(gè)"小頂堆".
   */
    private static <T> void siftUpComparable(int k, T x, Object[] es) {
        Comparable<? super T> key = (Comparable<? super T>) x;

        // 這個(gè)k就是我們傳進(jìn)來(lái)的隊(duì)列長(zhǎng)度,默認(rèn)是0
        while (k > 0) {
            // (k-1)除2, 求出k結(jié)點(diǎn)的父結(jié)點(diǎn)索引parent
            // 例如,k等于1或者2,那么k的父節(jié)點(diǎn)在數(shù)組位置0
            // 如果,k等于3,那么k的父節(jié)點(diǎn)在數(shù)組位置1
            int parent = (k - 1) >>> 1;

            // 取出父節(jié)點(diǎn)的元素
            Object e = es[parent];

            // 如果插入的元素值大于等于父結(jié)點(diǎn)元素值, 則退出“上浮”循環(huán)
            if (key.compareTo((T) e) >= 0)
                break;

            // 如果插入的元素值小于父結(jié)點(diǎn)元素值, 則把父節(jié)點(diǎn)放到k的位置
            es[k] = e;
            // 繼續(xù)向上找下一個(gè)父節(jié)點(diǎn)是否需要上浮調(diào)整
            k = parent;
        }

        // 上浮調(diào)整結(jié)束,找到適合元素key的位置k,保存到數(shù)組中
        es[k] = key;
    }

原來(lái),siftUpComparable方法的作用其實(shí)就是堆的“上浮調(diào)整”,可以把平衡二叉堆想象成一棵完全二叉樹,每次插入元素都鏈接到二叉樹的最右下方,然后將插入的元素與其父結(jié)點(diǎn)比較,如果父結(jié)點(diǎn)大,則交換元素,直到?jīng)]有父結(jié)點(diǎn)比插入的結(jié)點(diǎn)大為止。這樣就保證了堆頂(二叉樹的根結(jié)點(diǎn))一定是最小的元素。(注:以上僅針對(duì)“小頂堆”)

再看看siftUpUsingComparator,只是if那句換成if (key.compareTo((T) e) >= 0)而已,所以就不再重復(fù)闡述了。

計(jì)算機(jī)的二叉樹就是倒過(guò)來(lái)的樹
上浮過(guò)程

如果換成自定義的優(yōu)先級(jí)比較器??梢韵胂筱y行的各種金卡黑卡普通卡排隊(duì)比較。只要金卡來(lái)了,就會(huì)排比黑卡、普通卡排前面。黑卡來(lái)了也可以插普通卡的隊(duì)。

“有錢大曬?。俊?br> “抱歉,有錢是真的能為所欲為的?!?/p>

抱歉,有錢是真的能為所欲為的

這種操作就是折半法查找,每找一次排除一半的可能,256個(gè)數(shù)據(jù)中查找只要找8次就可以找到目標(biāo),2^x =N,所以時(shí)間復(fù)雜度x=Ο(logN)。

ok,總結(jié)下今天的結(jié)論:

  • PriorityQueue的默認(rèn)優(yōu)先級(jí)是數(shù)值從小到大排序
  • 排序比較的過(guò)程就是堆的上浮處理過(guò)程,可以想象銀行金卡、黑卡各種插隊(duì)普通卡
  • 上浮算法的關(guān)鍵是通過(guò) (k - 1) / 2 找到k的父節(jié)點(diǎn),再根據(jù)優(yōu)先級(jí)是否調(diào)換位置
  • 時(shí)間復(fù)雜度是Ο(logN)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容