Java8 PriorityQueue 源码阅读
一、什么是 PriorityQueue
這篇文章帶大家去了解一個 jdk 中不常用的數據結構 PriorityQueue(優先隊列),雖然在項目里用的不多,但是它本身的設計實現還是很值得大家看一看的。
PriorityQueue 底層是一個用數組實現的完全二叉樹,但它并不只是一個完全二叉樹,在沒有自定義比較器(自然排序)的情況下,更嚴格的來講它是一個基于數組實現的小頂堆(父節點的元素值小于左右孩子節點的元素值)。
二、PriorityQueue 簡介
2.1 相關屬性
我們知道在 HashMap 中有很多屬性,比如默認的初始化大小,加載因子等,在 PriorityQueue 中也有很多這種常量。下面我們來認識一下,這有助于我們更好的理解 PriorityQueue。
// 優先隊列的默認初始大小是 11private static final int DEFAULT_INITIAL_CAPACITY = 11;// 底層是一個 Object 數組transient Object[] queue;// 優先隊列中的元素個數private int size = 0;// 最大容量限制private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;...2.2 相關構造函數
PriorityQueue 有很多構造函數,但是有一個重要的構造函數,這里把它貼出來,為什么說它重要呢,因為大部分構造函數(并不是全部)內部都是通過調用這個構造函數進行初始化的。
public PriorityQueue(int initialCapacity,Comparator<? super E> comparator) {// Note: This restriction of at least one is not actually needed,// but continues for 1.5 compatibilityif (initialCapacity < 1)throw new IllegalArgumentException();// 初始化底層 queue 數組this.queue = new Object[initialCapacity];// 如果自定義比較器則初始化自定義比較器this.comparator = comparator;}三、擴容機制
PriorityQueue 的擴容機制其實很簡單,如下:
private void grow(int minCapacity) {int oldCapacity = queue.length;// Double size if small; else grow by 50%// 當容量小于 64 時容量為原來的兩倍 + 2,如果大于等于 64 時擴容為原來的 1.5 倍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);}四、添加元素
上面我們已經說了 PriorityQueue 在自然排序下(不自定義比較器)是一個用數組實現的小頂堆,下面我們就來看一下其內部具體是怎么實現的。
添加元素的方法有兩個,分別是 add 與 offer,但是 add 內部調用的是 offer 方法,這里我們只對 offer 方法進行分析。
由于新插入元素后,可能會導致小頂堆的結構被破壞,因此需要將新插入的元素(在小頂堆的最低層)向上調整,如果插入的元素比父節點大,那么就把父節點調下來,記錄父節點的位置后繼續向上調整,直到其比父節點元素值大為止。
public boolean offer(E e) {// 插入元素為 null,直接拋出異常if (e == null)throw new NullPointerException();modCount++;// 記錄優先隊列中的元素個數(也是新元素的插入位置),判斷是否需要擴容int i = size;// 當數組容量不夠時進行擴容if (i >= queue.length)grow(i + 1);// size + 1size = i + 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);elsesiftUpComparable(k, x);}上面 siftUp 方法中做了一個判斷,主要是判斷用戶有沒有自定義比較器,由于這兩個方法類似,這里我們只看其中一個沒有自定義比較器的 siftUpComparable 方法。
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;}理解了原理,看代碼的過程就比較容易了,沒有很多的代碼,花點時間仔細看一下就能理解了。
五、移除元素
移除元素的方法也有兩個,分別是 remove 與 poll,與 remove 不同的是 poll 每次移除的是堆頂的元素,也就是最小的元素,remove 可以移除指定的任意元素,并且這個移除只會移除第一次出現的該元素,如果后面也有該元素是不會移除的。
poll 方法相比較于 remove 方法要簡單一些,因為 poll 每次移除的是堆頂的元素,那么在調整二叉堆的時候只需要從頭開始調整就好了(把隊尾的元素移到隊首),如果孩子節點比父節點小,就把較小的孩子節點移到父節點的位置,記錄移動的孩子的節點的位置,繼續向下調整即可,而 remove 方法移除的元素可能是介于堆頂與堆尾的元素,這時就不僅需要向下調整了,必要的時候也需要向上進行調整才能維持小頂堆。
這里只貼出 poll 的方法,相信你對 poll 方法理解了,remove 方法耐心看下來肯定也是可以理解的。
@SuppressWarnings("unchecked")public E poll() {if (size == 0)return null;// size 減 1int s = --size;modCount++;// 獲取隊首的元素E result = (E) queue[0];// 獲取隊尾的元素(隊首元素被移除,把隊尾元素放在隊首,從上往下調整二叉堆)E x = (E) queue[s];// 隊尾元素置 nullqueue[s] = null;if (s != 0)// 調整二叉堆siftDown(0, x);return result;}siftDown 如下:
private void siftDown(int k, E x) {// 判斷是否自定義了比較器if (comparator != null)siftDownUsingComparator(k, x);elsesiftDownComparable(k, x);}同 offer 方法一樣,poll 方法在調整小頂堆時也分了自然排序與自定義排序兩種情況,這里我們仍然只了解其中一個自然排序的方法 siftDownComparable。
private void siftDownComparable(int k, E x) {Comparable<? super E> key = (Comparable<? super E>)x;int half = size >>> 1; // loop while a non-leafwhile (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;}關于 PriorityQueue 我們就先了解這么多,如果你有興趣可以從頭到尾把對應的源碼看一遍~
PS:因為 PriorityQueue 在初始化的時候允許用戶傳入自定義比較器,因此你也可以自定義比較器使其成為一個大頂堆,就不在這里演示了。
jdk1.8 源碼閱讀:https://github.com/zchen96/jdk1.8-source-code-read
參考資料
參考了這篇博客的部分內容,圖畫的很好,講解的也很好,感謝此博主大大~?@CarpenterLee
總結
以上是生活随笔為你收集整理的Java8 PriorityQueue 源码阅读的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中国人民志愿军第十三兵团炮兵指挥所旧址属
- 下一篇: 美国变态军人杀战友是什么电影