1. 程式人生 > 實用技巧 >(未完)Java集合框架梳理(基於JDK1.8)

(未完)Java集合框架梳理(基於JDK1.8)

Java集合類主要由兩個介面CollectionMap派生出來的,Collection派生出了三個子介面:ListSetQueue(Java5新增的佇列),因此Java集合大致也可分成ListSetQueueMap四種介面體系

Java集合框架大致示意圖(包含常用的集合類和大致的介面繼承關係):

根介面Collection:

根介面Map:

四種體系分別代表瞭如下內容:

  • List : 有序可重複集合
  • Queue : 佇列集合
  • Set : 無序不可重複集合
  • Map : 鍵值對集合

其中, 所有Collection介面的實現類均可以使用的方法有
contains(Object o)

: 判斷集合中是否存在指定元素
containsAll(Collection<?> c) : 判斷集合中是否包含指定Collection的所有元素
isEmpty() : 判斷集合是否為空
iterator() : 獲取Collection物件的迭代器
size() : 獲取集合中的元素個數
toArray() : 返回包含所有集合元素的Object陣列
toArray(T[] a) : 返回指定型別的陣列,如果a足夠大,則使用該陣列儲存元素,並把多餘的空間設定為null;否則返回一個新的同類型陣列。例如可以使用下面的語句獲得一個String型別陣列:

String[] strs = collection.toArray(new String[0]);

List介面

基於List的集合允許元素的重複,同時提供了隨機訪問的方法,可以對集合中的任意元素進行操作。

ArrayList[1]

ArrayList簡介

ArrayList 是一個數組佇列,內部基於陣列實現,相當於動態陣列。與Java中的陣列相比,它的容量能動態增長。它繼承於AbstractList,實現了List, RandomAccess, Cloneable, java.io.Serializable這些介面。

ArrayList 實現了RandmoAccess介面,即提供了隨機訪問功能。RandmoAccess是java中用來被List實現,為List提供快速訪問功能的。在ArrayList中,我們即可以通過元素的序號快速獲取元素物件;這就是快速隨機訪問。

由於ArrayList基於陣列實現,那麼它也有陣列的一些性質,例如:

  • 支援快速隨機訪問,即對元素的查、改可以在\(O(1)\)時間完成
  • 在陣列中間的增刪操作需要移動陣列,時間效率不佳
  • 需要佔據連續的記憶體空間

和Vector不同,ArrayList中的操作不是執行緒安全的!所以,建議在單執行緒中才使用ArrayList,而在多執行緒中可以選擇Vector或者CopyOnWriteArrayList

ArrayList建構函式

ArrayList中提供了三種建構函式:

  • ArrayList() : 預設建構函式
  • ArrayList(int initialCapacity) : 傳入一個初始的容量(預設值為10)
  • ArrayList(Collection c) : 使用指定的Collection構造ArrayList

ArrayList原始碼分析

1. 成員屬性elementDatasize

elementData是一個Object型別的陣列,它就是ArrayList儲存元素的方式。它的初始容量預設為10。當集合中的元素超出這個容量,便會進行擴容操作。需要擴容時,ArrayList會把陣列容量擴大一半。

擴容操作也是ArrayList 的一個性能消耗比較大的地方,所以若我們可以提前預知資料的規模,應該通過ArrayList(int initialCapacity)構造方法,指定集合的大小,去構建ArrayList例項,以減少擴容次數,提高效率。

size就是動態陣列的實際大小,即儲存了多少個元素。

2. 常用操作
1. 增
  • 先判斷是否越界,是否需要擴容。
    • 如果擴容, 就複製陣列。
    • 然後設定對應下標元素值。

值得注意的是:

  • 如果需要擴容的話,預設擴容一半。如果擴容一半不夠,就用目標的size作為擴容後的容量。
2. 刪
  • 刪除操作會修改modCount,且可能涉及到陣列的複製,相對低效。
  • 批量刪除中,涉及高效的儲存兩個集合公有元素的演算法,可以留意一下。
3. 改

不會修改modCount,相對增刪是高效的操作。

public E set(int index, E element) {
    rangeCheck(index);//越界檢查
    E oldValue = elementData(index); //取出元素 
    elementData[index] = element;//覆蓋元素
    return oldValue;//返回元素
}
4. 查

陣列本身支援快速隨機訪問,效率很高。

public E get(int index) {
    rangeCheck(index);//越界檢查
    return elementData(index); //下標取資料
}
E elementData(int index) {
    return (E) elementData[index];
}
3. 小結
  • 擴容操作會導致陣列複製,批量刪除會導致 找出兩個集合的交集,以及陣列複製操作,因此,增、刪都相對低效。 而 改、查都是很高效的操作。
  • 與ArrayList相似的Vector的內部也是陣列做的,區別在於Vector在API上都加了synchronized所以它是執行緒安全的,以及Vector擴容時,是翻倍size,而ArrayList是擴容50%。

LinkedList

LinkedList簡介

除了List介面外,LinkedList還實現了Deque介面,提供了add/remove、offer/poll等方法,可以作為一個佇列或雙端佇列來使用。

顧名思義,LinkedList是基於連結串列實現,這也使它具有連結串列的特點,即對結點的插入和刪除效率更高,但隨機訪問效能較差。同時由於使用連結串列實現,不需要連續的記憶體空間,也不需要考慮擴容的問題。

和ArrayList一樣,LinkedList也不是執行緒安全的

LinkedList原始碼分析

1. 類的定義
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

從這段程式碼中我們可以清晰地看出 LinkedList 繼承 AbstractSequentialList,實現 ListDequeCloneableSerializable。其中 AbstractSequentialList 提供了 List 介面的骨幹實現,從而最大限度地減少了實現受“連續訪問”資料儲存(如連結列表)支援的此介面所需的工作,從而以減少實現 List 介面的複雜度。Deque是一個線性 collection,支援在兩端插入和移除元素,定義了雙端佇列的操作。

LinkedList類中有三個屬性,分別為sizefirstlast

  • int size : 儲存當前列表中的元素個數
  • Node<E> first : 儲存雙向連結串列的第一個節點
  • Node<E> last : 儲存雙向連結串列的最後一個節點
2. 提供的方法

作為List,LinkedList可以使用get(int index)獲取指定位置的元素,使用add(E e)向尾部新增元素,使用remove(int index)來刪除指定位置的元素。

LinkedList還可以作為雙端佇列使用。它實現了Deque介面,因此提供了佇列和雙端佇列的操作方法。

由於LinkedList基於連結串列實現,它不能實現快速隨機訪問,而是需要用遍歷連結串列的方式進行。獲取指定位置的元素的實現方式如下:

// 獲取index位置的結點
Node<E> node(int index) {
    // assert isElementIndex(index);
    // 前半部分的結點從頭部開始遍歷;後半部分的結點從尾部開始遍歷
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

LinkedList更多是用於佇列或雙端佇列,這部分內容在Queue介面的總結中介紹。

Queue介面

在Queue介面下有三個常用的類,分別為LinkedList, ArrayDeque, PriorityQueue

其中,Queue有一個子介面DequeLinkedListArrayDeque都是基於該介面實現。

Queue和Deque介面的通用方法:

作為佇列,常用的操作為從佇列的一端入隊、從另一端出隊,以及檢視下一個要出隊的元素。

Queue中有兩套操作的方法,都能實現相同的功能,但其中一套是遇到問題時(例如從空佇列取元素或向滿佇列新增元素)丟擲異常,另一套則是返回特殊值(false/null)

  1. 丟擲異常型

    add()方法向末尾新增元素,remove()從頭部取出元素,element()檢視頭部元素。

  2. 返回特殊值型

    offer()方法向末尾新增元素,poll()從頭部取出元素,peek()檢視頭部元素。

雙端佇列Deque繼承了Queue介面,因此它也能夠進行上述操作,不過它提供了更為具體的方法,可以自由選擇對頭部還是對尾部操作。可以直接體現在方法名上。對於上述方法,直接在其後加上First/Last,例如:

  • addLast(), removeFirst(), offerLast(), pollFirst(), peekFirst(), getFirst()
  • addFirst(), removeLast(), offerFirst(), pollLast(), peekLast(), getLast()

其中第一行對應了Queue中的方法,第二行則是Queue中無法實現的操作。除了getFirst()getLast()替代了Queue中的element()外,其餘的方法都是在原有方法上加字尾。

LinkedList

LinkedList實現了List和Deque兩個介面,基於連結串列,允許插入null。關於List介面的部分已經介紹完畢,這裡介紹它對Deque介面的實現。

LinkedList中關於新增和刪除元素的操作都是基於linkFirst(), linkLast(), unlinkFirst(), unlinkLast()完成的。

linkFirst()的實現如下:

private void linkFirst(E e) {
    final Node<E> f = first;    // 獲取頭結點
    final Node<E> newNode = new Node<>(null, e, f); // 根據元素值建立新結點
    first = newNode; // 將新結點設為頭結點
    // 建立頭結點和原頭結點的連線
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

linkLast()則是對尾結點進行操作,其餘和上述實現相同。除此之外,還有一個linkBefore()方法,實現向連結串列的中間插入,它除了需要多建立一道連線外,與上述方法也類似。

從連結串列中刪除結點的方法有unlinkFirst()unlinkLast()以及unlink(),其中unlink()用於刪除任意結點,其他兩個用於刪除首位結點。

例如,unlinkFirst()的實現如下:

// 這裡的f是上一個呼叫函式傳入的,並且一定為頭結點。
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

其他兩個方法的實現類似。

對於操作頭結點的public方法,例如addFirst(), removeFirst(), offerFirst(), pollFirst(), remove()poll()等,最終都會去呼叫linkFirst()unlinkFirst()進行實際的結點操作,區別在於在操作前可以對集合狀態以及輸入引數的合法性進行檢查。
反之那些對尾結點進行操作的方法,最終都會呼叫linkLast()unlinkLast()進行實際的結點操作。

ArrayDeque

ArrayDeque是Deque的另一個實現類,這個類從JDK1.6開始引入,它和ArrayList類似,底層都基於陣列實現。ArrayDeque把陣列看作迴圈陣列進行處理,以便能夠更好地支援雙端佇列的特性,即從頭部和尾部都能夠方便地操作元素。

注意:

  • ArrayDeque不支援插入null值
  • ArrayDeque不是執行緒安全的

在實現佇列和堆疊的時候,ArrayDeque的效能要優於LinkedList,非常重要的原因是LinkedList每插入一個元素都要使用new來建立一個結點,這會帶來很大的開銷;同時因為連結串列結構的記憶體地址不連續,很難命中快取,這就導致了從主存讀取的又一大的時間開銷。因此如果只需要實現雙端佇列的功能,使用ArrayDeque通常可以獲得更好的效能。

但需要注意的是,並不是所有情況下都適合使用ArrayDeque,例如當你必須在佇列中儲存null值時就需要使用LinkedList(儘管官方不推薦null值);另外由於ArrayDeque沒有實現List介面,它也沒有提供隨機訪問的方法,因此如果需要使用List的相關功能,ArrayDeque就無法提供。

ArrayDeqeu建構函式

ArrayDeque提供了三種建構函式,分別為預設構造、從已有Collection中構造,以及指定預設容量。下面主要介紹第三種。

在ArrayDeque原始碼中該建構函式實現如下:

public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

// 建立一個數組用於存放元素
private void allocateElements(int numElements) {
    elements = new Object[calculateSize(numElements)];
}

// 根據元素數量來確定初始的陣列容量
private static int calculateSize(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1); // 將高2位設定為1
        initialCapacity |= (initialCapacity >>>  2); // 將高4位設定為1
        initialCapacity |= (initialCapacity >>>  4); // 將高8位設定為1
        initialCapacity |= (initialCapacity >>>  8); // 將高16位設定為1
        initialCapacity |= (initialCapacity >>> 16); // 將高32位設定為1
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    return initialCapacity;
}

我們重點關注private static int calculateSize(int numElements)這一方法。這個方法的作用是將陣列的實際容量控制為大於且最接近指定值的2的整數次方。在位運算中,距離一個數最近的2的整數次冪就是比這個數高一位的位元為1,其餘為0。
通過五次邏輯右移操作,可以確保把最高的1bit位和它的右邊全部設定為1。然後再+1,就可以得到一個距離它最近的而且大於它的2的整數次冪了。

ArrayDeque屬性和方法

在ArrayDeque中有用來存放集合元素的Object類的陣列elements;頭元素的下標head和尾元素的下標tail

ArrayDeque的元素個數並不是用專門的成員變數來儲存,而是使用如下方法計算返回:

public int size() {
    return (tail - head) & (elements.length - 1);
}

由於陣列的容量是比元素數大的2的整數次冪,那麼elements.length - 1就是將最高位的右側全部置1,其他位為0。這個與運算實際上相當於取模的操作。因為ArrayDeque作為迴圈陣列,頭元素是會向前增長過0,從而迴圈到到陣列的末尾的,所以頭結點的下標可能比尾結點更大。

1. 插入元素

ArrayDeque提供了雙端佇列提供的所有插入方法,實際起作用的方法有兩個,分別是在頭部插入的addFirst()和在尾部插入的addLast()

addFirst()方法的原始碼如下:

public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

在頭部插入,會先把head減1,並且取模,保證了下標可以迴圈。然後將元素放入陣列的對應下標位置。

接下來判斷首位結點是否相遇,如果相遇說明需要進行擴容,這時把容量擴充為原來的2倍。doubleCapacity()方法的實現如下:

private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    head = 0;
    tail = n;
}

擴容的過程大概分為以下步驟[2]

  1. 檢測tail是否等於head;
  2. 建立一個原陣列容量2倍的新陣列;
  3. 將原陣列從head位置一分為二,分別拷貝到新陣列中;比如陣列容量16,head位置是7,則先將原陣列下標從7開始,拷貝16-7個元素到新的下標從0開始,元素個數為16-7的陣列中。然後將原陣列下標從0開始到7的元素拷貝到新陣列下標從(16-7)開始,元素個數是7的陣列中。這麼做的目的就是為了拷貝到新陣列的時候保持元素原來的順序。
  4. 重新確定頭尾指標的位置。

向尾部插入的addLast()方法類似:

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}
2. 刪除元素

刪除元素使用的方法為pollFirst()pollLast(),分別為從頭部和從尾部刪除元素,在佇列為空時返回null(這也是ArrayDeqeu不允許存放空值的原因之一)。其他刪除方法直接呼叫這兩個方法或在此基礎上檢查佇列為空時丟擲異常。

pollFirst()的實現如下:

public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    // Element is null if deque empty
    if (result == null)
        return null;
    elements[h] = null;     // Must null out slot
    head = (h + 1) & (elements.length - 1);
    return result;
}

獲取元素的方式很簡單,利用頭部下標從陣列中取出即可。同時將head+1並取模(迴圈陣列),與插入恰好相反。而pollLast()與這個方法區別不大,就不再贅述。

小結

總體來說, ArrayDeque是實現棧和佇列以及雙端佇列的首選,它不需要從中間刪除結點,因此通過迴圈陣列下標儲存頭尾結點的結構保障了它的高效能。它從首位讀取元素的時間都是\(O(1)\),並且不需要建立新結點,而且由於陣列記憶體空間連續的特點,連續的讀取也會有較高的快取命中率。

它的缺點是由於容量必須為2的冪次,也一定程度上降低了空間的利用率,在最壞的情況下,如果元素最大個數為\(2^k\),就要實際建立一個容量為\(2^{k+1}\)的陣列,浪費了一半的空間。

PriorityQueue

PriorityQueue,即優先順序佇列,繼承自AbstractQueue,即可以實現Queue介面的所有方法。Java中的PriorityQueue是通過堆來實現的,可以通過構造方法傳入比較器來決定如何判定優先順序。預設為最小值堆,即佇列首部的元素就是整個佇列的最小值,如果有多個就取其中之一。

堆是一棵完全二叉樹,而由於完全二叉樹的性質決定了它可以使用順序儲存結構通過隨機訪問來高效實現,因此PriorityQueue的底層資料結構是陣列,也有capacity,有擴容操作。

同樣,PriorityQueue也不是執行緒安全的。

屬性

private static final int DEFAULT_INITIAL_CAPACITY = 11; //預設的初始化容量

transient Object[] queue; //存放元素的陣列,非私有方便巢狀類訪問

private int size = 0; //元素個數

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

transient int modCount = 0; // 用於實現 fast-fail 機制的

構造方法

PriorityQueue提供了多達7個構造方法,如下:

//預設無參構造器,採用初始化容量 11,無 comparator
public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

//只傳入指定的初始化容量,不傳入 comparator
public PriorityQueue(int initialCapacity) {
    this(initialCapacity, null);
}

//只傳入 comparator,採用預設容量 11
public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}

//傳入使用者指定的初始化容量和 comparator
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;
}

//傳入一個集合(非SortedSet/PriorityQueue或它的子類物件)
@SuppressWarnings("unchecked")
public PriorityQueue(Collection<? extends E> c) {
    // 如果傳入的集合可以被SortedSet例項化,就獲取它的比較器
    if (c instanceof SortedSet<?>) {
        // 先進行型別轉換
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        initElementsFromCollection(ss); 
    }
    // 如果傳入的集合可以被PriorityQueue示例化,那麼也獲取比較器
    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);
    }
}

//傳入一個 PriorityQueue 型別的集合
@SuppressWarnings("unchecked")
public PriorityQueue(PriorityQueue<? extends E> c) {
    this.comparator = (Comparator<? super E>) c.comparator();
    initFromPriorityQueue(c);
}

//傳入 SortedSet 型別的集合
@SuppressWarnings("unchecked")
public PriorityQueue(SortedSet<? extends E> c) {
    this.comparator = (Comparator<? super E>) c.comparator();
    initElementsFromCollection(c);
}

在上述構造方法中,使用了三種方法來從傳入的集合中初始化PriorityQueue,分別是initElementsFromCollection()initFromCollection()initFromPriorityQueue()

// 從PriorityQueue中初始化,如果型別相同就直接從原佇列中拿資料
private void initFromPriorityQueue(PriorityQueue<? extends E> c) {
    if (c.getClass() == PriorityQueue.class) {
        this.queue = c.toArray();
        this.size = c.size();
    } else {
        initFromCollection(c);
    }
}

// 從原來的集合中初始化,由於直接拿過來的元素不滿足優先佇列順序,初始化之後需要建堆
private void initFromCollection(Collection<? extends E> c) {
    initElementsFromCollection(c);
    heapify(); //建堆
}

// 從集合中初始化元素的方法
private void initElementsFromCollection(Collection<? extends E> c) {
    Object[] a = c.toArray(); //集合中的陣列內容
    // If c.toArray incorrectly doesn't return Object[], copy it.
    if (a.getClass() != Object[].class)
        a = Arrays.copyOf(a, a.length, Object[].class);
    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 = a;
    this.size = a.length;
}

前面提到了堆是完全二叉樹,可以使用陣列實現。把堆的結點按照層次順序儲存到陣列中,就可以通過計算下標的方式訪問到它的父結點和左右子結點。對於下標為\(i\) 的結點:

  1. 父結點為:\(i/2\ (i ≠ 1)\),若i = 1,則i是根節點。
  2. 左子結點:\(2i\ (2i ≤ n)\), 若不滿足則無左子結點。
  3. 右子結點:\(2i + 1\ (2i + 1 ≤ n)\),若不滿足則無右子結點。

在PriorityQueue中,為了維持堆的特性,最重要的兩個操作就是shiftUpshiftDown

首先我們來看shiftUp()相關方法:

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

//有比較器的時候用這個
@SuppressWarnings("unchecked")
private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1; //找到當前節點 k 的父節點
        Object e = queue[parent]; //取父節點
        if (comparator.compare(x, (E) e) >= 0)
            break;
        // 如果父節點優先順序小於它,就和它交換,並繼續判斷父節點是否需要上移,
        // 直到它的優先順序小於父結點或者到達了根結點
        queue[k] = e; 
        k = parent;
    }
    queue[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 = parent;
    }
    queue[k] = key;
}

再來看shiftDown()相關方法:

// 把下標為k的元素x下拉
private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

// 有比較器的情況
@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
    // 根據完全二叉樹性質,size / 2就是第一個葉子結點的座標。
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;   // 2k+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;
    } // 終止條件是k已經是葉節點或者k小於其所有子節點
    queue[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;
        queue[k] = c;
        k = child;
    }
    queue[k] = key;
}

有了上述兩個方法之後,我們再來看構造方法中的建堆操作heapify():

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

可以看出,建堆的過程就是從非葉子結點開始,自底向上地將每個結點執行一次下拉操作。這樣就可以保證每次都把優先順序高的結點交換到上層,直到優先順序最高的被交換到堆頂。

堆的插入和刪除

1. 插入結點(入隊)

新結點的插入,具體的實現在offer()方法中:

public boolean offer(E e) {
    if (e == null)          //如果需要插入的資料為 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);       // 否則先插入尾部,然後通過shiftUp找到對應位置。
    return true;
}

總的來說,新結點的插入總是在尾部進行,但是插入之後會通過shiftUp向上找到合適的位置。

2. 刪除結點(出隊)

優先佇列的出隊每次都是取出頭結點,即索引為0的結點。

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

從具體的實現方式可以看出,刪除頭結點後需要找到一個新的頭結點。而最方便的做法就是先將尾結點提到頭結點,然後再依次執行shiftDown,就可以讓每個結點都到對應的位置來。

3. 刪除任意元素

在PriorityQueue中,還實現了一個remove(Object o)方法,可以刪除堆中的指定元素。

public boolean remove(Object o) {
    // 找到元素的下標
    int i = indexOf(o);
    if (i == -1)
        return false;
    else {
        removeAt(i);
        return true;
    }
}

// 刪除指定下標的元素
private E removeAt(int i) {
    // assert i >= 0 && i < size;
    modCount++;
    int s = --size;                 // 獲取隊尾元素,並調整size
    if (s == i)                     // 如果只有一個元素了,就直接置null
        queue[i] = null;
    else {
        E moved = (E) queue[s];     // 用隊尾元素替代被刪除元素
        queue[s] = null;
        siftDown(i, moved);         // shiftDown到合適位置
        if (queue[i] == moved) {    
            siftUp(i, moved);       // 如果沒有被下拉,就嘗試進行shiftUp
            if (queue[i] != moved)
                return moved;
        }
    }
    return null;
}

刪除堆中指定元素的步驟大致概括為:

  1. 找到要刪除的元素的座標
  2. 用隊尾元素替換要被刪除的元素
  3. 從刪除位置開始執行shiftDown
  4. 如果shiftDown沒有效果(即它不會比目前位置更低),就執行shiftUp

  1. ArrayList部分參考自CSDN: https://blog.csdn.net/zxt0601/article/details/77281231 ↩︎

  2. ArrayDeque部分內容參考自簡書: https://www.jianshu.com/p/b29a516eb322 ↩︎