1. 程式人生 > 其它 >JDK原始碼閱讀(十四) : 優先順序佇列——PriorityQueue

JDK原始碼閱讀(十四) : 優先順序佇列——PriorityQueue

技術標籤:jdk原始碼閱讀java資料結構佇列

1. 優先順序佇列

PriorityQueue是一種基於無界優先順序佇列。內部使用Object陣列儲存資料,在容量不足時會進行擴容操作。內部元素的排序規則,按照構造例項時傳入的Comparator或者元素自身的排序規則(所屬類實現Comparable介面)。

2. Fields

預設的陣列長度為11。

private static final int DEFAULT_INITIAL_CAPACITY = 11;

用於儲存資料的Object陣列。

transient Object[] queue;

元素數量。

private int
size = 0;

給元素排序的排序器,如果沒有自定義排序器,則會使用元素自身的排序規則。

private final Comparator<? super E> comparator;

容器修改次數。

transient int modCount = 0;

3. Constructors

(1)空參構造器。使用預設的陣列長度和元素自身的排序規則。

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

(2)自定義陣列長度的構造器。

public PriorityQueue(int initialCapacity)
{ this(initialCapacity, null); }

(3)自定義元素排序規則的構造器。

public PriorityQueue(Comparator<? super E> comparator) {
    this(DEFAULT_INITIAL_CAPACITY, comparator);
}

(4)此外,還有其他構造器,可以自定義多種配置資訊。

4. 新增元素

add方法向優先順序佇列中新增元素。如果新增成功,會返回true。如果添加了null,會丟擲空指標異常

public boolean add(E e) {
    return offer
(e); }
public boolean offer(E e) {
  	//不可以新增null	
    if (e == null)
        throw new NullPointerException();
    modCount++;
  	//獲取當前元素數
    int i = size;
  	//如果超出了陣列長度,則陣列擴容
    if (i >= queue.length)
        grow(i + 1);
  	//元素總數加1
    size = i + 1;
  	//如果當前容器為空,則直接在0號位置放置元素
    if (i == 0)
        queue[0] = e;
    else
      	//否則,將新元素插入到堆中,實行向上調整
        siftUp(i, e);
    return true;
}

【擴容策略】:如果舊陣列的長度小於64,則新陣列的長度變成舊陣列的2倍+2;如果舊陣列的長度大於等於64,則新陣列的長度為舊陣列的1.5倍

private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    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);
}

siftUp方法將新元素插入堆中時,根據是否使用自定義的排序器來執行不同的方法。

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

siftUpComparable方法使用了元素自身的排序規則。前提是這個元素的所屬類必須已經實現Comparable介面。

private void siftUpComparable(int k, E x) {
  	//將新元素的型別轉換為Comparable
    Comparable<? super E> key = (C<? super E>) x;
  	//起始時,k指向最後一個元素的後一位
  	//向上調整堆,直到到達堆頂
    while (k > 0) {
      	//堆是一棵完全二叉樹
      	//取得最後一個元素的父親節點(父節點的座標為i,左子節點的座標為2*i+1,右子節點的座標為2*i+2)
      	//通過上述規則,如果某個節點的位置為k,則其父節點的位置為(k-1)/2
        int parent = (k - 1) >>> 1;
      	//取得父節點元素
        Object e = queue[parent];
      	//如果當前節點比父節點大,則可以直接插入在當前位置k上
        if (key.compareTo((E) e) >= 0)
            break;
      	//否則,就將父節點元素移動到當前位置k
        queue[k] = e;
      	//k指向父節點
        k = parent;
      	//在迴圈中,不斷向上調整,直到遇到當前節點比父節點大,或者,k到達了頂點,就能退出迴圈了
    }
  	//最後,k為插入位置
    queue[k] = key;
}

siftUpUsingComparator方法使用自定義排序器進行插入,與上述方法類似。

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)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

5. 彈出堆頂元素

remove方法定義在PriorityQueue類的抽象父類AbstractQueue中,其內部也是呼叫了PriorityQueue類自身實現的poll方法

在佇列為空時做彈出操作,remove方法會丟擲NoSuchElementException;而poll方法,會返回null。

public E remove() {
    E x = poll();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}
public E poll() {
    if (size == 0)
        return null;
    int s = --size;
    modCount++;
  	//彈出的是堆頂元素
    E result = (E) queue[0];
  	//取得最後一個元素
    E x = (E) queue[s];
  	//將最後一個位置置為null,表示刪除
    queue[s] = null;
    if (s != 0)
      	//然後將前面取得的最後一個位置的元素放到堆頂,再向下調整堆
        siftDown(0, x);
    return result;
}
private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

siftDownComparable方法使用元素自身的排序規則向下調整堆。

private void siftDownComparable(int k, E x) {
  	//假設將最後一個元素key放在堆頂的位置,也就是k位置上
    Comparable<? super E> key = (Comparable<? super E>)x;
  	//取得最後一個父節點的下一個位置
    int half = size >>> 1;        // loop while a non-leaf
  	//只需要調整到最後一個父節點,就可以結束。
    while (k < half) {
      	//取得當前位置k的左孩子
        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];
      	//比較k位置的元素和其最小的孩子,如果key更小,則可以停止調整了
        if (key.compareTo((E) c) <= 0)
            break;
      	//否則,就將更小的孩子放到k位置上
        queue[k] = c;
      	//k下移到該孩子處
        k = child;
      	//不斷調整,直到最後一個父節點也調整完畢,或者當前的父節點已經比兩個孩子都小了,就退出迴圈
    }
  	//把key實際放入k位置上
    queue[k] = key;
}

6. 優先順序佇列的應用

(1)求top K個元素

在資料量很大的情況下,無法一次性將所有資料載入進記憶體進行排序來獲得topK個元素。此時,可以先將前面k個元素放入一個容量為k的優先順序佇列,然後一點一點把後面的資料載入到記憶體中,並嘗試插入堆中。

比如,我們要求最小K個元素,就建立一個大頂堆(也稱最大堆),堆頂元素是最大的。所以排序規則需要按照從大到小排序。

插入的時候,首先比較堆頂元素,如果大於等於堆頂,就不用插入了;如果小於堆頂,就將堆頂移除,然後將新元素插入堆中。

使用這種方式,可以動態地求得top K個元素。

//numbers陣列儲存了全部資料
public void topK(int[] numbers, int k){
    int len;
    if(numbers == null || (len = numbers.length) == 0 || k > len){
        throw new IllegalArgumentException();
    }

    //建立一個長度為k的大頂堆
    Comparator<Integer> comparator = (o1, o2) -> {
        if (o1.compareTo(o2) == -1) {
            return 1;
        } else if (o1.equals(o2)) {
            return 0;
        } else {
            return -1;
        }
    };
  
    //優先順序佇列就是一個堆,傳入自定義的排序器,從大到小排序
    PriorityQueue<Integer> pq = new PriorityQueue<>(k,comparator);

    //將前k個元素插入堆中,時間複雜度為O(klogk)
    for(int i = 0; i < k; ++i){
        pq.add(numbers[i]);
    }

    //將剩餘元素插入到堆中,時間複雜度O(nlogk)
    for(int i = k; i < len; ++i){
        //取得堆頂元素
        Integer peek = pq.peek();
        //如果當前元素比堆頂元素小,就將堆頂元素移除,將當前元素插入
        if(numbers[i] < peek){
            pq.remove();
            pq.add(numbers[i]);
        }
    }

    //輸出堆中的元素
    for(int i : pq){
        System.out.print(i + " ");
    }

}

(2)求動態資料的中位數

可以用兩個堆來求中位數。資料總數為n。

如果n為偶數,則將前n/2個元素放入到一個大頂堆中,將後n/2個元素放入到一個小頂堆中。此時,兩個堆的堆頂是中位數。

如果n為奇數,則將前(n+1)/2個元素放入到一個大頂堆中,將後(n-1)/2個元素放入到一個小頂堆中。此時,大頂堆的堆頂是中位數。

//建兩個堆,一個是大頂堆,一個是小頂堆
private PriorityQueue<Integer> maxpq = new PriorityQueue<>((o1, o2) -> {
    if (o1.compareTo(o2) == -1) {
        return 1;
    } else if (o1.equals(o2)) {
        return 0;
    } else {
        return -1;
    }
});
private PriorityQueue<Integer> minpq = new PriorityQueue<>();
//numbers陣列用於儲存資料
private int[] numbers;
//size儲存資料總數
private int size;

將已有的資料放入到兩個堆中。

public void initHeaps(){
    //將前一半資料放入大頂堆
    int mid = (size - 1) >>> 1;
    for(int i = 0; i <= mid; ++i){
        maxpq.add(numbers[i]);
    }
	//遍歷後一半資料,如果比大頂堆的堆頂元素小,就將這個堆頂元素移動到小頂堆,並將當前資料放入大頂堆
    for(int i = mid+1; i < size; ++i){
        Integer peek = maxpq.peek();
        if(peek > numbers[i]){
            Integer move = maxpq.remove();
            maxpq.add(numbers[i]);
            minpq.add(move);
        }else {//否則,就將當前資料直接放入小頂堆
            minpq.add(numbers[i]);
        }
    }
}

如果陣列中存在動態新增元素的操作,中位數不斷地變化,那麼在新增元素時,如果新元素小於大頂堆的堆頂元素,則將其插入到大頂堆;如果大於等於大頂堆的堆頂元素,則插入到小頂堆。但是,這種做法可能會導致兩個堆不平衡,此時只需要在每次插入操作後,對兩個堆進行調整,將數量多的堆的堆頂元素不斷移動到數量少的堆,直到兩個堆平衡。時間複雜度為O(logn)。

public void add(int e){
    ensureCapacity(size+1);
    numbers[size++] = e;

    //插入到兩個堆中
    Integer peek = maxpq.peek();
    if(peek == null) {
        maxpq.add(e);
    }else {
        if (e < peek) {
            maxpq.add(e);
        } else {
            minpq.add(e);
        }
    }
    //調整兩個堆的大小
    balanceTwoHeaps();
}

調整兩個堆的大小。始終保持大頂堆比小頂堆多一個,或者兩個堆數量相等。

private void balanceTwoHeaps() {
    int size1 = maxpq.size();
    int size2 = minpq.size();
    while(!(size1 == size2) && !(size1 == size2 + 1)){
        if(size1 > size2){
            minpq.add(maxpq.remove());
            --size1;
            ++size2;
        }else {
            maxpq.add(minpq.remove());
            ++size1;
            --size2;
        }
    }
}

獲取中位數。如果資料總數是奇數,則大頂堆的堆頂元素為中位數。如果是偶數,則兩個堆頂的元素為中位數。時間複雜度為O(1)。

public void getMiddle(){
    if((size&1) == 1){
        System.out.println(maxpq.peek());
    }else {
        System.out.println(maxpq.peek() + " " + minpq.peek());
    }
}

【舉一反三】:取中位數可以用兩個堆來實現,取其他位置的元素同樣可以用兩個堆來實現。