Java容器之PriorityQueue源碼分析(附堆的調整圖解)

  PriorityQueue容器,翻一下就是優先隊列,平常一般稱優先隊列的作用是將隊列中的元素最小值放到堆頂小頂堆,最小的元素在頂端,你也可以修改comparator,使之變成大頂堆,最大的元素在頂端)。每當我們插入刪除元素,都是從堆頂進行,優先隊列會自動重新調整,將最小的元素值調整到堆頂

  本篇博客將從源碼的角度並結合相應的圖解,對Java中的PriorityQueue容器的實現原理進行分析。爲了幫助大家更好的理解PriorityQueue,寫了一篇關於堆的實現與原理分析博客,牆裂推薦各位小夥伴閱讀做鋪墊,鏈接→數據結構之堆(我猜,關於堆的這些維護細節,你肯定不清楚,不信你來看!)

註明:以下源碼分析都是基於jdk 1.8.0_221版本
在這裏插入圖片描述

一、PriorityQueue容器概述

  PriorityQueue類的申明如下:

public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable

在這裏插入圖片描述
  Java中的PriorityQueue容器,底層是通過數組來實現堆結構。在前一篇博客數據結構之堆(我猜,關於堆的這些維護細節,你肯定不清楚,不信你來看!)說過,基於數組實現的堆兩個重要規律:

  1. 下標爲index的左、右孩子的下標分別是index * 2 + 1(index + 1) * 2
  2. 堆中有子節點的節點最大下標爲 size / 2 - 1(注意:size爲堆的大小,不是數組的大小)

在這裏插入圖片描述

二、PriorityQueue類中的主要屬性

/**
 * 數組默認初始化的長度
 */
private static final int DEFAULT_INITIAL_CAPACITY = 11;

/**
 * 實現堆的數組
 */
transient Object[] queue; // non-private to simplify nested class access

/**
 * 優先隊列中的元素數(注意需要與數組的長度區分開)
 */
private int size = 0;

/**
 * 優先隊列中元素比較器,通過指定comparator,可以選擇構造小頂堆還是大頂堆
 */
private final Comparator<? super E> comparator;

/**
 * 結構性調整(插入、刪除、擴容等操作)的次數
 */
transient int modCount = 0;

三、PriorityQueue類的構造器

/**
 * 默認構造器,數組長度初始化爲默認值
 */
public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

/**
 * 指定數組的初始化長度
 */
public PriorityQueue(int initialCapacity) {
    this(initialCapacity, null);
}

/**
 * 指定元素比較器
 */
public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}

/**
 * 數組長度、元素比較器都指定
 */
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
    // 數組長度不能小於1
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

/**
 * 複製構造器1
 * 將容器1中的元素全部放入當前創建的優先隊列中
 */
@SuppressWarnings("unchecked")
public PriorityQueue(Collection<? extends E> c) {
    if (c instanceof SortedSet<?>) {
    	// 如果容器C是SortedSet接口實現類,可初始化comparator
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        // 在將容器中的元素全部插入堆中
        initElementsFromCollection(ss);
    }
    else if (c instanceof PriorityQueue<?>) {
    	// 如果容器c是PriorityQueue的子類,直接賦值comparator、pq即可
        PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        initFromPriorityQueue(pq);
    }
    else {
    	// 否則comparator無法初始化,只能期望c中的元素實現了comparable接口
        this.comparator = null;
        initFromCollection(c);
    }
}

/**
 * 複製構造器2
 */
@SuppressWarnings("unchecked")
public PriorityQueue(PriorityQueue<? extends E> c) {
	// 如果容器c是PriorityQueue的子類,直接賦值comparator、pq即可
    this.comparator = (Comparator<? super E>) c.comparator();
    initFromPriorityQueue(c);
}

/**
 * 複製構造器3
 */
@SuppressWarnings("unchecked")
public PriorityQueue(SortedSet<? extends E> c) {
	// 如果容器C是SortedSet接口實現類,可初始化comparator
    this.comparator = (Comparator<? super E>) c.comparator();
    initElementsFromCollection(c);
}

四、PriorityQueue容器中的堆維護方法

1、擴容

/**
 * 基於數組實現的堆,只要複製到一個更長的數組即可
 */
private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // 如果oldCapacity < 64,擴大爲原來的2倍,否則擴大原來的一半
    int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1));
    // 如果newCapacity超過了最大值MAX_ARRAY_SIZE,則調用hugeCapacity方法
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 然後將原數組擴複製更長的數組
    queue = Arrays.copyOf(queue, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
	// 當minCapacity >= 2^31時,由於最高位是符號位,並且是補碼存儲,所以此時minCapacity < 0
    if (minCapacity < 0)
        throw new OutOfMemoryError();
    // 返回max(Integer.MAX_VALUE, MAX_ARRAY_SIZE)
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

2、插入元素(上浮

  在優先隊列插入元素,先放入數組中的下一個空閒位置,然後根據下標的邏輯關係,將插入的元素上浮
在這裏插入圖片描述

/**
 * 往優先隊列中插入一個元素
 */
public boolean add(E e) {
    return offer(e);
}

/**
 * 往優先隊列中插入一個元素
 */
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;
    // 如果i == 0,說明插入前,優先隊列中沒有元素,此時直接放入堆頂,無需進行調整
    if (i == 0)
        queue[0] = e;
    else
    // 否則調用siftUp方法,將e放入queue[i],然後上浮插入元素
        siftUp(i, e);
    return true;
}

/**
 * 將x放入queue[k],然後上浮插入元素
 */
private void siftUp(int k, E x) {
	// 如果優先隊列容器指定了comparator,則使用comparator進行上浮
	// 否則只能寄託於元素E的類型實現了Comparable接口
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}
/**
 * 使用Comparable中的compare方法進行上浮插入節點
 */
@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
	// 傳入時的k是堆的尾端
    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],並且將插入節點index轉移到父節點的下標,繼續上浮
        queue[k] = e;
        k = parent;
    }
    // 退出while循環時,k是真正應該插入的位置,將key賦值到該下標對應的位置即可
    queue[k] = key;
}
/**
 * 使用comparator比較器進行上浮插入節點,與上面的方法是一樣的
 */
@SuppressWarnings("unchecked")
private void siftUpUsingComparator(int k, E x) {
	// 傳入時的k是堆的尾端
    while (k > 0) {
    	// 計算當前插入位置的邏輯父節點下標
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        // 如果父節點比插入節點小,停止上浮
        if (comparator.compare(x, (E) e) >= 0)
            break;
        // 否則將父節點放入queue[k],並且將插入節點index轉移到父節點的下標,繼續上浮
        queue[k] = e;
        k = parent;
    }
    // 退出while循環時,k是真正應該插入的位置,將key賦值到該下標對應的位置即可
    queue[k] = x;
}

3、刪除元素(下沉

  Java中的PriorityQueue不但實現了刪除頂端元素的接口,而且還實現了刪除任意位置元素的接口。不過兩者的操作是相似的,先用堆尾元素替換刪除元素,然後對替換後的元素進行下沉操作。
在這裏插入圖片描述

/**
 * 移除堆頂元素
 */
@SuppressWarnings("unchecked")
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)
    	// 將x放入queue[0],並下沉元素
        siftDown(0, x);
    return result;
}
/**
 * 將元素x放入queue[x]然後下沉queue[x]
 */
private void siftDown(int k, E x) {
	// 如果優先隊列容器指定了comparator,則使用comparator進行上浮
	// 否則只能寄託於元素E的類型實現了Comparable接口
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}
/**
 * 使用Comparable中的compare方法進行下沉元素
 */
@SuppressWarnings("unchecked")
private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;
    // size >>> 1 - 1是最大的有子節點的節點下標
    int half = size >>> 1;
    // 當k < half,此時說明queue[k]在邏輯上存在子節點,可能需要下沉
    while (k < half) {
    	// (k << 1) + 1爲k的左子節點下標
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        // 如果存在右子節點,並且左子節點 > 右子節點,則更新下標child爲右子節點的下標
        if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            c = queue[child = right];
        // 現在c指向左右子節點較小值,child指向較小者的下標
        // 如果key <= 左右子節點較小者,停止下沉
        if (key.compareTo((E) c) <= 0)
            break;
        // 將左右子節點較小值移動到queue[k],然後更新k爲較小者的下標,繼續下沉
        queue[k] = c;
        k = child;
    }
    // 退出while循環後,說明key需要放入queue[k],不需要再下沉
    queue[k] = key;
}
/**
 * 使用comparator比較器進行下沉元素,與上面的方法是一樣的
 */
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
	// size >>> 1 - 1是最大的有子節點的節點下標
    int half = size >>> 1;
    // 當k < half,此時說明queue[k]在邏輯上存在子節點,可能需要下沉
    while (k < half) {
    	// (k << 1) + 1爲k的左子節點下標
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        // 如果存在右子節點,並且左子節點 > 右子節點,則更新下標child爲右子節點的下標
        if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        // 現在c指向左右子節點較小值,child指向較小者的下標
        // 如果key <= 左右子節點較小者,停止下沉
        if (comparator.compare(x, (E) c) <= 0)
            break;
        // 將左右子節點較小值移動到queue[k],然後更新k爲較小者的下標,繼續下沉
        queue[k] = c;
        k = child;
    }
    // 退出while循環後,說明key需要放入queue[k],不需要再下沉
    queue[k] = x;
}

4、初始化

  前面在介紹PriorityQueue的構造器的時候,有幾個複製構造器,其中一個是從Collection實現類中提取元素放入堆中。PriorityQueue類並沒有一個一個的放入堆中,再調整,而是一次性讀取所有元素,然後統一調整。

/**
 * 從Collection接口實現類中進行初始化
 */
private void initFromCollection(Collection<? extends E> c) {
	// 將容器c中的元素提取到queue數組
    initElementsFromCollection(c);
    // 進行統一的堆調整
    heapify();
}
/**
 * 將容器c中的元素提取到queue數組
 */
private void initElementsFromCollection(Collection<? extends E> c) {
    Object[] a = c.toArray();
    // 可能c.toArray返回的不是Object[]數組,在強轉複製一遍
    if (a.getClass() != Object[].class)
        a = Arrays.copyOf(a, a.length, Object[].class);
    // 檢查數組中是否存在null對象
    int len = a.length;
    if (len == 1 || this.comparator != null)
        for (int i = 0; i < len; i++)
            if (a[i] == null)
                throw new NullPointerException();
    // 修改this對象的queue引用,size大小(這裏並沒有調整堆)
    this.queue = a;
    this.size = a.length;
}
/**
 * 調整整個queue數組
 */
@SuppressWarnings("unchecked")
private void heapify() {
	// (size >>> 1) - 1是最大的有子節點的節點下標
    for (int i = (size >>> 1) - 1; i >= 0; i--)
    	// 從底往上,對每一個有子節點的元素進行下沉操作(前面刪除元素介紹過這個方法,不要跳着看哦。。。)
        siftDown(i, (E) queue[i]);
}

五、總結

  經過上面的源碼分析,可以看出PriorityQueue容器的本質就是封裝了queue數組,每次插入元素放到堆尾,然後上浮插入元素;每次刪除堆頂元素時,將堆尾元素移動到堆頂,然後下沉該元素。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章