1. 程式人生 > 其它 >PriorityQueue原始碼分析,基於JDK1.8詳細分析

PriorityQueue原始碼分析,基於JDK1.8詳細分析

閱讀本文章之前推薦先閱讀博主關於堆排序相關的內容,傳送地址:八大排序演算法大彙總 原理+圖解+原始碼+複雜度分析

PriorityQueue原始碼分析

文章目錄

一、堆結構

  1. 滿二叉樹:所有層都達到最大節點數的二叉樹

    image-20210425233955040
  2. 完全二叉樹:所有葉子節點都在最後一層或者倒數第二層,而且最後一層的葉子節點在左邊連續,倒數第二層的葉子節點在右邊連續,稱為完全二叉樹,也就是說新增結點時需要把每一層從左往右填滿之後才可在下一層從左往右新增結點

    通俗的理解為:完全二叉樹是指除了最後一層外其它層都達到最大節點數,且最後一層節點都靠左排列

    img

    左孩子結點的索引:2 * 當前節點的索引 + 1

    右孩子結點的索引:2 * 當前結點的索引 + 2

    父節點的索引:(當前結點的索引 - 1) / 2

    結點數為n,則層數為logn (父與子結點的索引關係以2倍為基礎)

  3. 堆:就是一顆完全二叉樹

  4. 大頂堆:每個結點的值都大於或等於其左右孩子結點的值,沒有要求左右孩子結點的大小關係,堆頂是最大值

  5. 小頂堆:每個節點的值都小於或等於其左右孩子結點的值,沒有要求左右孩子結點的大小關係,堆頂是最小值

  6. 一個長度為size的堆結構,如果一個節點的索引值index大於等於 size >>> 1 時,認為該節點為葉子節點,否則就是非葉子節點

  7. 優先順序佇列就是堆結構,Java中無Heap類表示堆結構,使用PriorityQueue類表示堆結構:

    (1) 此類支援泛型

    (2) 此類的構造器引數中可以傳遞一個實現了Comparator介面的類,表示往堆中新增元素時按照此類的compare方法中的排序方式加入;注意:優先順序佇列預設是小頂堆

二、基本介紹

  • 優先順序佇列,是0個或多個元素的集合,集合中的每個元素都有一個權重值,每次出隊都彈出權重值最大或最小的元素

  • 優先順序佇列使用堆來實現,預設是小頂堆

  • 不可以新增取值為null的元素

  • PriorityQueue是非執行緒安全的

  • PriorityQueue不是有序的,只有堆頂儲存著最小或最大的元素

  • 繼承體系

    Queue 的實現類有 ArrayDeque、LinkedList、PriorityQueue

    image-20210426091929910
  • Queue 介面中定義瞭如下方法:

    image-20210426101914933

    PriorityQueue 中也實現了這些方法

三、原始碼分析

1. 主要屬性

//預設容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;

//儲存元素的地方
transient Object[] queue;

//實際元素個數
int size;

//比較器
private final Comparator<? super E> comparator;

//修改次數
transient int modCount;

觀察上述屬性可得到如下結論:

  • 優先順序佇列的預設容量是11
  • 實際儲存元素的結構是Object型別的陣列(堆一般使用陣列來儲存)
  • 在優先順序佇列中,有兩種方式比較元素的大小,一種是元素的自然順序(預設為小頂堆),另一種是通過比較器比較
    • 如果使用預設方式,要求新增的元素必須實現Comparable介面
  • 擁有 modCount 屬性,表示優先順序佇列有快速失敗的特性

2. 構造方法

常用的構造方法如下所示:

//空參構造器,使用預設的長度為11的容量,沒有定義比較器
public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null); 
}

//指定初始容量的構造器,沒有定義比較器
public PriorityQueue(int initialCapacity) {
    this(initialCapacity, null);
}

//指定比較器,且使用預設容量11的構造器
public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}

//指定初始容量及比較器的構造器
public PriorityQueue(int initialCapacity,
                     Comparator<? super E> comparator) {
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

//指定集合的構造器
public PriorityQueue(Collection<? extends E> c) {
    if (c instanceof SortedSet<?>) {
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator(); //使用引數的比較器
        initElementsFromCollection(ss); //從引數集合中初始化佇列元素
    }
    else if (c instanceof PriorityQueue<?>) {
        PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator(); //使用引數的比較器
        initFromPriorityQueue(pq); //從引數集合中初始化佇列元素
    }
    else {
        this.comparator = null;
        initFromCollection(c);
        //沒有指定比較器,從引數集合中初始化佇列元素後調整成為小頂堆
    }
}

3. 入隊方法

3.1 add方法

add(E e) 原始碼如下:

public boolean add(E e) {
    return offer(e); //呼叫offer方法
}

3.2 offer方法

offer(E e) 原始碼如下:

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException(); //不支援null元素
    
    modCount++;
    int i = size; //i的值比最後一個元素的索引值大1,也就是待插入位置的索引
    
    //元素個數超過了佇列的最大容量,呼叫grow方法擴容
    if (i >= queue.length)
        grow(i + 1); //擴容方法之後講述
    
    //呼叫siftUp方法,將元素新增到佇列尾部並重新調整成為小頂堆(預設情況下)
    siftUp(i, e);
    
    size = i + 1;
    return true;
}

siftUp(int k, E x) 方法原始碼如下:

//k表示待插入位置的索引,x表示待插入元素
private void siftUp(int k, E x) {
    
    //根據是否有比較器而呼叫不同的方法
    
    if (comparator != null)
        siftUpUsingComparator(k, x, queue, comparator);
    else
        siftUpComparable(k, x, queue);
}

siftUpComparable(int k, T x, Object[] es) 方法原始碼如下:

/**
* 引數k表示size,比當前最後一個元素的索引值大1,即待插入位置的索引
* 引數x表示要新增到隊尾的元素
* 引數es表示儲存元素的陣列
*/
private static <T> void siftUpComparable(int k, T x, Object[] es) {
    Comparable<? super T> key = (Comparable<? super T>) x; //key表示要新增的元素
    
    //如果k==0,即堆為空時,不進入while迴圈,直接將元素賦予陣列首位置
    while (k > 0) {
        
        //元素將要新增的位置就是size,即k
        
        //找到父節點的索引
        int parent = (k - 1) >>> 1;
        
        //e表示父節點的值
        Object e = es[parent];
        
        //如果要新增元素的值大於父節點的值,則跳出迴圈,已經滿足小頂堆的要求
        if (key.compareTo((T) e) >= 0)
            break;
        
        //如果比父節點的值小,則需要與父節點交換位置,其實就是將父節點移動到了原本屬於此元素的位置
        es[k] = e;
        
        //新插入的元素成為父節點,繼續執行迴圈與父節點比較
        k = parent;
    }
    //找到該插入的位置,將元素插入
    es[k] = key;
}

觀察上述原始碼,可以得到如下結論:

  • 不允許新增null元素
  • 如果陣列不夠用了,先擴容
  • 如果堆中還沒有元素,新新增的元素成為首元素
  • 如果堆中已經有元素了,就插入到最後一個元素往後的一個位置(此時還沒有實際插入)並重新調整成為小頂堆(持續與父節點比較,找到合適的位置再將元素實際插入)
  • 為什麼PriorityQueue中的add(e)方法沒有做異常檢查呢?
    • 因為PriorityQueue是無限增長的佇列,元素不夠用了會擴容,所以新增元素不會失敗

4. 擴容方法

grow(int minCapacity) 方法原始碼如下:

//引數minCapacity的取值為擴容之前的size + 1
private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    
    //如果舊容量小於64,新容量為舊容量的2倍+2
    //如果舊容量大於等於64,新容量為舊容量的1.5倍
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                     (oldCapacity + 2) :
                                     (oldCapacity >> 1));
    
    //如果擴容後陣列大小溢位
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    
    //創建出一個新容量大小的新陣列並把舊陣列元素拷貝過去
    queue = Arrays.copyOf(queue, newCapacity);
}

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

5. 出隊方法

5.1 remove()方法

remove() 方法存在於AbstractQueue類中,彈出隊首的元素,原始碼如下:

public E remove() {    E x = poll(); //呼叫poll方法移除隊首的元素    if (x != null)        return x;    else        throw new NoSuchElementException(); //如果沒有隊首元素,則丟擲異常}

5.2 poll()方法

poll() 方法原始碼如下,彈出隊首的元素:

思路:首先將陣列最後一個元素的值儲存起來(假設儲存在x中),對最後一個元素賦予null值(相當於長度減一),使用x從首元素的子節點開始比較,有兩種情況:

  • 如果x小於兩個子節點,已滿足小頂堆的要求,則x成為父節點,此時原首元素被覆蓋
  • 如果子節點有比x小的,此子節點成為父節點,此時原首元素被覆蓋;x成為子節點繼續向下比較,直到找到合適的位置插入
public E poll() {
    final Object[] es;
    final E result;

    //當佇列不為空時執行if語句
    if ((result = (E) ((es = queue)[0])) != null) {
        
        //es表示儲存元素的陣列
        //result表示隊首元素,一開始就儲存起來,所以即使後期首元素被覆蓋也可以返回原首元素
        
        modCount++;
        final int n;
        final E x = (E) es[(n = --size)];  
        //n表示size-1,即最後一個元素的索引
        //x表示陣列最後一個元素
        
        es[n] = null; //將最後一個元素清空
        
        //若佇列中有兩個元素及以上時才執行if
        if (n > 0) {
            
            //根據是否定義比較器從而呼叫不同的方法
            
            final Comparator<? super E> cmp;
            if ((cmp = comparator) == null)
                siftDownComparable(0, x, es, n);
            else
                siftDownUsingComparator(0, x, es, n, cmp);
            	//過程與siftDownComparable方法類似,不再贅述,唯一的區別是比較時使用的是傳遞進來的Comparator比較器中的compare方法
        }
    }
    return result; //返回隊首元素,若沒有隊首元素不拋異常,返回null
}

siftDownComparable() 方法原始碼如下:

/**
* 引數k取值為0,隊首索引,也就是將要被刪除的元素的索引
* 引數x表示陣列最後一個元素,在最後一個元素被賦予null值之前已將其儲存至x
* 引數es表示儲存元素的陣列
* 引數n表示size-1
*/
//此時陣列的最後一個元素為null,但原來最後一個元素的值被賦予null值之前已將其儲存至x
private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
    Comparable<? super T> key = (Comparable<? super T>)x;
    int half = n >>> 1; //用於保證是非葉子節點
    
    //僅與非葉子節點比較
    while (k < half) {
        int child = (k << 1) + 1; //child左孩子節點索引值
        Object c = es[child]; //c表示左孩子節點的值
        int right = child + 1; //right表示右孩子節點索引值
        
        //不越界的情況下,左右孩子節點取其小者賦予c,較小者的索引賦予child
        if (right < n &&
            ((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
            c = es[child = right];
        
        //x(之前已儲存的最後一個元素)如果比兩個子節點的值都小,停止比較,已滿足小頂堆
        if (key.compareTo((T) c) <= 0)
            break;
        
        //如果比最小的子節點大,則交換位置,較小者成為父節點
        es[k] = c;
        
        //原來的父節點移到最小子節點的位置繼續往下比較
        k = child;
    }
    
    //找到正確位置放入x(之前已儲存的最後一個元素)
    es[k] = key;
}

6. 刪除方法

6.1 remove(Object o)方法

過程與移除隊首元素類似,不同之處在於最後一個元素不是從根節點的子節點開始比較,而是從待刪除元素的子節點開始比較

remove(Object o) 原始碼如下:

public boolean remove(Object o) {
    
    //呼叫indexOf方法得到要刪除元素的索引值,若沒有此元素返回-1
    int i = indexOf(o); 
    if (i == -1)
        return false; //刪除失敗
    else {
        removeAt(i); //呼叫removeAt方法刪除元素
        return true;
    }
}

indexOf(Object o) 方法原始碼如下:

private int indexOf(Object o) {
    if (o != null) {
        final Object[] es = queue;
        for (int i = 0, n = size; i < n; i++)
            if (o.equals(es[i]))
                return i;
    }
    return -1; //若沒有此元素返回-1
}

removeAt(int i) 原始碼如下:

E removeAt(int i) {
    final Object[] es = queue;
    modCount++;
    int s = --size; //s表示size-1,即最後一個元素的索引
    
    //如果索引為最後一個元素,則刪除佇列最後一個元素即可
    if (s == i) 
        es[i] = null;
    
    //如果刪除的不是最後一個元素
    else {
        E moved = (E) es[s]; //moved為最後一個元素
        es[s] = null; //將佇列最後一個元素置為null,相當於長度減一
        
        //呼叫siftDown方法,引數i表示要被刪除元素的索引,moved為最後一個元素
        siftDown(i, moved);
        //執行完之後i成為最後一個元素插入的位置,moved仍然是被儲存起來的最後一個元素
        
        //如果執行siftDown向下調整沒有改變佇列
        if (es[i] == moved) {
            
            siftUp(i, moved); //向上調整
            //從當前節點開始與父節點比較大小,如果比父節點大,則停止比較,否則持續向上比較,直到找到合適的位置插入,過程類似於反向的siftDown,不再贅述
            
            if (es[i] != moved)
                return moved;
        }
    }
    return null;
}

方法原始碼如下:

private void siftDown(int k, E x) {        //根據是否定義比較器從而呼叫不同的方法        if (comparator != null)        siftDownUsingComparator(k, x, queue, size, comparator);    else        siftDownComparable(k, x, queue, size); //此方法不再贅述}

7. 檢視隊首元素方法

7.1 element()方法

element() 方法原始碼如下:

public E element() {    E x = peek(); //呼叫peek方法    if (x != null)        return x;    else        throw new NoSuchElementException(); //沒有隊首元素時丟擲異常}

7.2 peek()方法

peek() 方法原始碼如下:

public E peek() {    return (E) queue[0]; //返回隊首元素,若沒有隊首元素不拋異常}

四 、總結

  • 入隊就是堆的插入元素的實現
  • 出隊就是堆的刪除元素的實現