Java數據結構之PriorityQueue

前言

Queue(隊列)是擁有先進先出(FIFO)特性的數據結構,PriorityQueue(優先級隊列)是它的子類之一,不一樣於先進先出,它能夠經過比較器控制元素的輸出順序(優先級)。本文就來分析一下PriorityQueuede的源碼,看看它是如何實現的。java

類繼承關係

先來看Queue接口:數組

public interface Queue<E> extends Collection<E> 複製代碼

Queue接口繼承了Collection接口,表示集合。它提供了三種方法,即:增長、刪除、獲取,每種都有兩個實現。安全

// 增長元素
boolean add(E e);
boolean offer(E e);
// 刪除元素
E remove();
E poll();
// 獲取元素
E element();
E peek();
複製代碼

兩種實現的區別是,addremoveelement都比對應的offerpollpeek多拋出一個異常。對於add方法,若是由於容量不足以增長新元素,會拋出IllegalStateException異常,而offer則會返回false。對於remove方法,若是隊列是空的,則會拋出NoSuchElementException異常,而poll方法會返回null。對於element方法,若是隊列是空的,則會拋出NoSuchElementException異常,而peek方法會返回null。
微信

再看PriorityQueue類:數據結構

public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable 複製代碼

嗯,不是直接實現Queue,而是繼承了AbstractQueue類。又是熟悉的感受,AbstractQueue應該也是模板抽象類(和AbstractMap和AbstractList相似)。
來看AbstractQueue多線程

public abstract class AbstractQueue<E> extends AbstractCollection<E> implements Queue<E> 複製代碼

果真,繼承了AbstractCollection類和實現了Queue接口。既然是模板類那確定有模板方法。AbstractQueue源碼中只實現了addremoveelemet方法。函數

public boolean add(E e) {
        if (offer(e)) // 調用offer
        ...
    }
    
public E remove() {
        E x = poll(); // 調用poll
        ...
    }
    
public E element() {
        E x = peek(); // 調用peek
        ...
    }
複製代碼

能夠看到,它們分別調用了offerpollpeek。也就是說,若是要經過AbstractQueue實現隊列,則必須實現offerpollpeek方法。
下面就正緊的分析PriorityQueue的源碼了。源碼分析

PriorityQueue源碼分析

縱觀源碼,發現PriorityQueue是經過「極大優先級堆」實現的,即堆頂元素是優先級最大的元素。算是集成了大根堆和小根堆的功能。this

重要屬性

根據堆的特性,存儲結構確定是數組了;既然支持不一樣優先級,確定有比較器,也就是說支持自定義排序和順序排序。spa

// 默認容量11
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 堆的存儲結構,存儲元素
transient Object[] queue; // 不可序列化
// 當前存儲的元素數量
int size;
// 比較器,肯定優先級高低
private final Comparator<? super E> comparator;
// 避免OOM,數組能夠分配的最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
複製代碼

構造函數

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) {
        ... // 對應賦值
    }

public PriorityQueue(Collection<? extends E> c) {
        // 將c轉成堆
        if (c instanceof SortedSet<?>) {
            ... // SortedSet自帶比較器
        }
        else if (c instanceof PriorityQueue<?>) {
            ... // PriorityQueue自帶比較器
        }
        else {
            ...
        }
    }
    
public PriorityQueue(PriorityQueue<? extends E> c) {
        ... // 從PriorityQueue獲取比較器,將c轉成堆
    }
    
public PriorityQueue(SortedSet<? extends E> c) {
        ...// 從SortedSet獲取比較器,將c轉成堆
    }
複製代碼

重用方法

PriorityQueue是支持擴容的,先來看擴容方法:

// minCapacity表示須要的最小容量
private void grow(int minCapacity) {
        int oldCapacity = queue.length; // 獲取當前容量
        // Double size if small; else grow by 50%
        // 若是舊容量小於64,則增長舊容量+2的大小
        // 若是舊容量大於等於64,則增長舊容量的一半大小
        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); // 複製已存儲的數據
    }
複製代碼

每次擴展的容量大小仍是挺大的,要麼變爲原來的雙倍,要麼增加一半大小。

在看增長元素、刪除元素和獲取元素的方法以前,先了解如下幾點:

  1. 徹底二叉樹的最後一個非葉子結點的下標是 (n-2) / 2;
  2. 徹底二叉樹中若是一個非葉子結點的下標是i,則它的父結點下標是(i-1)/2,它的左孩子下標是 2 * i + 1,右孩子下標是 2 * i + 2;

offer方法

public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)
            grow(i + 1); // 若是超出當前容量,則進行擴容
        siftUp(i, e); // 新元素都是增長在數組尾部,而後進行上移操做,即構建堆
        size = i + 1; // 當前大小加1
        return true;
    }
複製代碼

siftUp方法最終會調用siftUpUsingComparator或者siftUpComparable方法,兩個實現相似,這裏直接看siftUpUsingComparator方法。

// 上移就是不斷和父結點比較
private static <T> void siftUpUsingComparator( int k, T x, Object[] es, Comparator<? super T> cmp) {
        while (k > 0) {
            int parent = (k - 1) >>> 1; // 父結點下標
            Object e = es[parent];
            if (cmp.compare(x, (T) e) >= 0) // 優先級高則繼續上移比較
                break;
            es[k] = e;
            k = parent;
        }
        es[k] = x;
    }
複製代碼

每次增長元素,都要保證堆序。

poll方法

public E poll() {
        final Object[] es;
        final E result;
        // 返回堆頂元素
        if ((result = (E) ((es = queue)[0])) != null) {
            modCount++;
            final int n;
            final E x = (E) es[(n = --size)]; // 把尾部元素換到第一個
            es[n] = null;
            if (n > 0) {
                final Comparator<? super E> cmp;
                if ((cmp = comparator) == null) // 天然序時,下移調整
                    siftDownComparable(0, x, es, n);
                else // 自定義序下移調整
                    siftDownUsingComparator(0, x, es, n, cmp);
            }
        }
        return result;
    }
複製代碼

poll方法會返回隊首元素(堆頂),並將元素從堆中刪除。刪除過程,是將第一個元素與最後一個元素進行替換,而後再進行下移調整操做。這裏直接看siftDownUsingComparator方法:

private static <T> void siftDownUsingComparator( int k, T x, Object[] es, int n, Comparator<? super T> cmp) {
        // assert n > 0;
        int half = n >>> 1; // 最後一個非葉子結點下標(由於size已經減1了)
        while (k < half) {
            int child = (k << 1) + 1; // 左孩子
            Object c = es[child];
            int right = child + 1; // 右孩子
            if (right < n && cmp.compare((T) c, (T) es[right]) > 0)
                c = es[child = right]; // 從左右孩子中挑選優先級高的
            if (cmp.compare(x, (T) c) <= 0)
                break;
            es[k] = c; // 將目標元素下移
            k = child;
        }
        es[k] = x;
    }
複製代碼

每次彈出隊首元素,都要進行堆調整。

remove方法

poll方法能夠看出是remove方法的特例,即固定刪除第一個元素。

public boolean remove(Object o) {
        int i = indexOf(o); // 找到待刪除元素位置
        if (i == -1)
            return false;
        else {
            removeAt(i); // 刪除指定位置元素
            return true;
        }
    }
複製代碼

調用了removeAt方法:

E removeAt(int i) {
        // assert i >= 0 && i < size;
        final Object[] es = queue;
        modCount++;
        int s = --size; // size已經減1
        if (s == i) // removed last element
            es[i] = null; // 已經刪除到最後一個元素
        else {
            E moved = (E) es[s]; // 尾元素
            es[s] = null;
            siftDown(i, moved); // 指定元素換尾元素,而後調整
            if (es[i] == moved) {
                siftUp(i, moved); // 若是指定位置換成了尾元素(沒有發生下移)則進行上移操做
                if (es[i] != moved)
                    return moved;
            }
        }
        return null; // 正常刪除時返回null
    }
複製代碼

總結

  1. PriorityQueue是基於最大優先級堆實現的,根據比較器的狀況能夠是大根堆或者小根堆;
  2. PriorityQueue不支持null;
  3. PriorityQueue不是線程安全的,多線程環境下可使用java.util.concurrent.PriorityBlockingQueue
  4. 使用iterator()遍歷時,不保證輸出的序列是有序的,其實遍歷的是存儲數組。

關注微信公衆號,最新技術乾貨實時推送

image
相關文章
相關標籤/搜索