天天看點

JAVA Queue 與 PriorityQueu源碼分析

什麼是 Queue

資料結構中的隊列,先進先出式的資料結構,所有新元素都插入隊列的末尾,移除元素都移除隊列的頭部。主要注意的時,Java中的Queue是一個接口。

Queue 源碼分析

public interface Queue<E> extends Collection<E> {

    boolean add(E e);   //往隊列插入元素,如果出現異常會抛出異常
  
    boolean offer(E e); //往隊列插入元素,如果出現異常則傳回false

    E remove(); //移除隊列元素,如果出現異常會抛出異常

    E poll();   //移除隊列元素,如果出現異常則傳回null

    E element();    //擷取隊列頭部元素,如果出現異常會抛出異常

    E peek();   //擷取隊列頭部元素,如果出現異常則傳回null
}
           

上面六個函數總體上分為兩類:安全的會進行容量檢查的(add,remove,element),如果隊列沒有值,則取元素會抛出IlleaglStatementException異常。不安全的不進行容量控制的(offer,poll,peek )。

AbstractQueue 是Queue 的抽象實作類, AbstractQueue 也繼承自 AbstractCollection 。AbstractQueue 實作的方法不多,主要就 add、remove、element 三個方法的操作失敗抛出了異常。

什麼是 PriorityQueue

PriorityQueue 優先級隊列, ,不同于普通的遵循FIFO(先進先出)規則的隊列,每次都選出優先級最高的元素出隊,優先隊列裡實際是維護了這樣的一個堆,通過堆使得每次取出的元素總是最小的(使用者可以自定義比較方法,相當于使用者設定優先級)。 PriorityQueue 是一個小頂堆,是非線程安全的;PriorityQueue不是有序的,隻有堆頂存儲着最小的元素;從資料的存儲結構看, PriorityQueue 一個數組;從邏輯結構看, PriorityQueue 是一棵平衡二叉樹。

什麼是 PriorityQueue

PriorityQueue 優先級隊列, ,不同于普通的遵循FIFO(先進先出)規則的隊列,每次都選出優先級最高的元素出隊,優先隊列裡實際是維護了這樣的一個堆,通過堆使得每次取出的元素總是最小的(使用者可以自定義比較方法,相當于使用者設定優先級)。 PriorityQueue 是一個小頂堆,是非線程安全的;PriorityQueue不是有序的,隻有堆頂存儲着最小的元素;從資料的存儲結構看, PriorityQueue 一個數組;從邏輯結構看, PriorityQueue 是一棵平衡二叉樹。

PriorityQueue 源碼分析

繼承關系

public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable{
        // 略
    }
           

PriorityQueue 繼承了AbstractQueue 類,而AbstractQueue 類實作了Queue接口。

主要屬性

// 預設容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 存儲元素的數組,Object類型的
transient Object[] queue;
// 元素個數
int size;
// 比較器
private final Comparator<? super E> comparator;
// 修改次數
transient int modCount; 
           

transient Object[] queue

源碼注釋

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.
           

優先級隊列是一個平衡的二叉樹堆,queue[n]的兩個子節點queue[2n+1] and queue[2(n+1)]。 優先級隊列 按照 comparator 進行排序或者 按自然順序排序。 如果 comparator 為null ,堆中每一個節點 n 以及 n的後代d, n<=d。假設隊列為非空,則具有最小值的元素位于queue [0]中。

常用構造函數

  • 1、建立一個優先隊列對象,預設大小,隊列中的元素按照自然順序排序。
public PriorityQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }
           
  • 2、建立一個指定大小的優先隊列對象,隊列中的元素按照自然順序排序。
public PriorityQueue(int initialCapacity) {
        this(initialCapacity, null);
    }
           
  • 3、建立一個預設大小(11)的優先隊列對象,隊列中的元素按照指定比較器進行排序。
public PriorityQueue(Comparator<? super E> comparator) {
        this(DEFAULT_INITIAL_CAPACITY, comparator);
    }
           
  • 4、根據指定的大小和比較器來建立一個優先隊列對象。
public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        // Note: This restriction of at least one is not actually needed,
        // but continues for 1.5 compatibility
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }
           

add(e)/offer(e)方法介紹

add方法

插入一個元素到優先隊列中

add方法的源代碼如下:

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

從源碼中可以看出add方法直接調用了offer方法。

offer(e)方法

public boolean offer(E e) {
    if (e == null)  //檢查是否為null, 如果為null 則抛空指針異常
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)  // 檢查容量是否足夠, 不足進行擴容
        grow(i + 1);    // 擴容函數
    siftUp(i, e);   // 在數組的末尾插入新的元素,向上調整,使之保持為二叉堆的特性。
    size = i + 1;
    return true;
}
           

1)首先檢查要添加的元素e是否為null,如果為null,則抛空指針異常,如果不為null,則進行2

2)判斷數組是否已滿,如果已滿,則進行擴容,否則将元素加入到數組末尾即可。

由于這個數組表示的是一個“二叉堆”,是以還需要進行相應的調整操作,使得這個數組在添加元素之後依然保持的是二叉堆的特性。

grow(int minCapacity)方法

//增加數組的容量
private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    //如果比較小,則擴容為原來的2倍,否則隻擴容為原來的1.5倍
    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);
}
           

hugeCapacity(int minCapacity) 方法

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
           

注:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

public static final int MAX_VALUE = 0x7fffffff;

當 大于 MAX_ARRAY_SIZE 時擴容記性了特殊處理

siftUp(int k, E x)方法

将元素x插入到queue[k]位置上,并進行調整使之具有二叉堆特性

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

siftUpUsingComparator和siftUpComparable源碼如下

從 位置 k 向數組起始位置 調整使之具有二叉堆特性,自下而上的堆化,

也就是 二叉堆插入元素思想操作:

可參見 拜托,面試别再問我堆(排序)了!

siftUpComparable(int k, E 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; // 找 parent 父節點
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0) // 比較 按 自然順序 排列
            break;
        queue[k] = e;   
        k = parent; 
    }
    queue[k] = key;
}
           

siftUpUsingComparator(int k, E x)方法

@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)  // 使用比較器 comparator
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}
           

poll()方法介紹

取出優先隊列中的第一個元素

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

poll方法:取出queue[0]元素,然後将queue[size-1]插入到queue[0],然後自上而下堆化來保持二叉堆的特性。

siftDown(int k, E x)方法

//将元素x存儲在queue[k],并進行相應的調整
private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}
           

siftDownComparable(int k, E 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;
}

           

siftDownUsingComparator(int k, E x)方法

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

           

remove(Object o) 方法介紹

移除指定元素

public boolean remove(Object o) {
    int i = indexOf(o);     // 判斷位置
    if (i == -1)    //沒有在數組中找到
        return false;
    else {
        removeAt(i);    //進行删除并調整
        return true;
    }
}
           

indexOf(Object o)方法

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

找到對象o在數組中出現的第一個索引

emoveAt(int 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;
        siftDown(i, moved); //從i向下調整堆
        if (queue[i] == moved) {    // 如果發現調整後 moved 還在 i 位置沒有下沉,向上調整看是否上浮
            siftUp(i, moved);
            if (queue[i] != moved)
                return moved;
        }
    }
    return null;
}
           

堆插入元素與删除堆頂元素操作

以下面這個對為例

JAVA Queue 與 PriorityQueu源碼分析

用數組來存儲為:

JAVA Queue 與 PriorityQueu源碼分析

插入元素

往堆中插入一個元素後,需要繼續滿足堆的兩個特性,即:

(1)堆是一顆完全二叉樹;

(2)堆中某個節點的值總是不大于(或不小于)其父節點的值。

把元素插入到最後一層最後一個節點往後一位的位置,也就是在數組的末尾插入新的元素

,插入之後可能不再滿足條件(2),這時候我們需要調整,自下而上的堆化。

上面那個堆插入元素2,我們把它放在9後面,這時不滿足條件(2)了,我們就需要堆化。(這是一個小頂堆)

JAVA Queue 與 PriorityQueu源碼分析

從插入元素的過程,我們知道每次與 n / ( 2 x ) n/(2^x) n/(2x) 的位置進行比較,是以,插入元素的時間複雜度為 O ( l o g n ) O(log n) O(logn)。

删除堆頂元素

删除了堆頂元素後,要使得還滿足堆的兩個特性。把最後一個元素移到根節點的位置,這時候就滿足條件(1),之後就需要堆化了使它滿足條件(2)

JAVA Queue 與 PriorityQueu源碼分析

從删除元素的過程,我們知道把最後一個元素拿到根節點後,每次與 2 n 2n 2n 和 ( 2 n + 1 ) (2n+1) (2n+1) 位置的元素比較,取其小者,是以,删除元素的時間複雜度也為 O ( l o g n ) O(log n) O(logn) 。

回到 remove(Object o) 方法 ,不是删除堆頂元而是删除指定元素,源碼中會先找Object o在數組中位置 i,再取出最後一個元素從i自上而下堆化,如果最後一個元素沒有下沉,會從i自下而上堆化

參考:

拜托,面試别再問我堆(排序)了!

https://mp.weixin.qq.com/s/AF2tMHfofG8b51yIyaIReg