優先級隊列PriorityQueue源碼分析

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,即可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程Netty源碼系列MySQL工作原理文章。

微信公衆號

1. 回顧

在上一篇文章中分享了堆這種數據結構,同時提到,堆可以用來對數據排序,也可以用來解決Top N、定時任務、優先級隊列等問題,今天要分享的是Java中優先級隊列PriorityQueue的源碼實現,看看堆在Java中的實際應用。需要說明的是,本文與上篇文章:重溫《數據結構與算法》之堆與堆排序 密切相關。

2. PriorityQueue

優先級隊列有兩個常用的操作:向隊列中添加元素、取出元素,這兩個操作的方法爲add(E e)和poll(),接下來將圍繞這兩個方法的源碼展開。

PriorityQueue最底層採用數組來存放數據,它有很多構造方法,如果使用無參的構造方法,那麼隊列的最大容量將會採用默認值11,當一直向隊列中添加元素時,如果達到了最大容量,那麼將會進行擴容。

另外,優先級隊列中添加的元素,一定是能比較的大小的元素,而如何比較大小呢?有兩種選擇,第一:在創建PriorityQueue時指定一個Comparator類型的比較器;第二:添加到隊列中的元素自身實現Comparable接口。使用無參構造方法時,優先級隊列內部的比較器爲null,因此在這種情況下,添加到隊列中的元素需要實現Comparable接口,否則將會出現異常。

// 存放數據
transient Object[] queue;

// 默認的初始容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;

public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

2.1 添加元素(add(E e))

向優先級隊列中添加元素,實際上就是向堆中插入一個元素,當插入一個元素後,爲了滿足堆的性質(父結點的值要麼都大於左右子結點,要麼都小於左右子結點),因此可能需要堆化。下面是Java中PriorityQueue的add(E e)方法實現,你可以對照着上篇文章中堆插入數據的過程來看。

public boolean add(E e) {
    // 直接調用offer(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;       // 將以存放的數據個數+1
    if (i == 0)     // 第一個元素就不需要判斷是否要堆化了,直接加入即可
        queue[0] = e;
    else
        siftUp(i, e);   // 從下往上堆化
    return true;
}

可以看到,核心代碼在siftUp() 方法中,該方法從名字上就能推測出,是從下往上進行堆化。代碼實現如下:

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);    // 如果指定了比較器,則採用指定的比較器來判斷元素的大小,從而進行堆化
    else
        siftUpComparable(k, x); // 沒有指定比較器,那添加到隊列中的元素必須實現了Comparable接口
}

這裏我們以 siftUpComparable() 方法爲例分析,其實這兩個方法的實現邏輯一樣,不同的是怎麼比較兩個元素的大小。

// 從下往上堆化
private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1; // 數組索引除以2,實際上就是計算父結點的索引
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0)  // 當前結點與父結點進行比較
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}

在從下往上堆化的過程中,先就算出父結點的位置,然後和父結點比較大小,根據比較的結果,判斷是否還需要繼續向上堆化。這段代碼和上一篇文章中堆化的代碼幾乎一樣,可以對照着來看。

2.2 取出元素(poll())

實際上從優先級隊列中取出元素的過程,就是刪除堆頂元素的過程。在刪除完堆頂元素後,爲了滿足堆的性質,因此需要進行堆化。比較簡單的做法就是,將數組中最後的一個元素搬到堆頂,然後再從上到下來進行堆化。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;
}

可以看到,核心代碼實現在siftDown() 方法中,該方法的作用就是從上往下堆化。

// 從上往下堆化
private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);  // 有比較器
    else
        siftDownComparable(k, x);   // 元素自己實現Compareable接口
}

有比較器和無比較器的實現邏輯幾乎一致,下面只以siftDownComparable() 方法爲例,看看從上往下的堆化過程。

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

從上往下堆化的過程當中,葉子節點是不需要進行堆化的,因此代碼中,先計算出了處於數組最後面的非葉子結點的位置:int half = size >>> 1 (>>>的含義是無符號右移, >>> 1 實際上就是除以2)。 接着分別將當前結點的值與左右兩個結點的值比較,判斷是否需要交換。這段代碼的邏輯和上篇文章中heapify()方法的邏輯是一致的,可以對照着來看。堆化完,最終堆頂的元素就又變成了優先級最大或者最小的元素了。

3 總結

優先級隊列PriorityQueue的底層實現,實際上就是堆的實現,底層採用數組來存放數據,在插入數據時,採用的是從下往上進行堆化;取出元素時,實際上就是刪除堆頂元素,這個過程採用的是從上往下進行堆化。

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