PriorityQueue原理分析——基於源碼

在業務場景中,處理一個任務隊列,可能須要依照某種優先級順序,這時,Java中的PriorityQueue(優先隊列)即可以派上用場。優先隊列的原理與堆排序密不可分,能夠參考我以前的一篇博客:html

堆排序總結與實現數組

原理

PriorityQueue中維護一個Queue[]數組,在邏輯上把它理解成一個小根堆或大根堆,即一個徹底二叉樹,每個三元組中父節點小於兩個孩子結點(小根堆,若是是大於則是大根堆)。本博客以小根堆來進行說明,由於PriorityQueue默認實現小根堆,即小的數先出隊,固然也能夠自定義Comparator實現大根堆。oop

  • 入隊:每次入隊時,把新元素掛在最後,從下往上遍歷調整成小根堆;
  • 出隊:每次出隊時,移除頂部元素,把最後的元素移到頂部,並從上往下遍歷調整成小根堆。

出隊

poll()方法以下:code

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

能夠看到,隊首元素 queue[0] 出隊,隊尾的元素 queue[s] 進入 siftDown(0, x) 方法進行堆調整。siftDown方法以下:htm

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}
//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;//找到了key應該放入的位置
        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;
}

能夠看到,這與堆排序中的堆調整一模一樣。blog

入隊

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

一樣,其核心在於 siftUp(i, e) 方法。以下所示:隊列

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等於size
        k = parent;
    }
    queue[k] = key;//最後在待插入位置賦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;
}

此方法,是一個不斷從父節點往子節點賦值的過程,直到找到適合放置插入結點值的位置。element

移除

removeAt 方法以下所示:rem

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

移除下標爲i的元素,至關於以 i 爲根節點的徹底二叉樹的出隊,因而執行 siftDown 方法調整最後一個元素 moved 的位置,即將該堆調整爲小根堆。調整完以後,若是 moved 沒有來到 i 的位置,說明 i 以上的堆結構必定符合規則;若是 moved 被調整到 i 位置,i上面的父節點有可能比 moved大,因此須要 siftUp(i, moved) 方法從 i 位置向上調整,調整爲小根堆,完畢。

總結

其實無論是 siftUp 方法仍是 siftDown 方法,都是利用了徹底二叉樹的性質,經過父節點與孩子結點之間的快速訪問來實現的。

相關文章
相關標籤/搜索