最近在項目開發中開發了全雙工系統非同步長串連的通訊群組件,內部用到了延遲隊列。而延遲隊列的內部實現的儲存是用到了優先隊列,當時看C++的資料結構時,瞭解過優先隊列,用的儲存是二叉樹的邏輯,應該叫完全二叉樹,也可以叫做最大堆。
下面看一下二叉樹的演算法,主要看插入和刪除。
二叉樹顧名思義就像一棵樹,每個節點下最多可以掛兩個節點,如圖
在優先隊列中儲存的方式就是 queue = {A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q};
若當前節點下標為i 那麼它的子節點 左側 = 2*i+1 右側 = 2*i +2。優先隊列顧名思義,就是優先權最大的排在隊列的頭部,而優先權的判斷是根據對象的compare方法比較擷取的,保證根節點的優先順序一定比子節點的優先順序大。所以放入到優先隊列的元素要麼實現了Comparable介面,要麼在創造這個優先隊列時,指定一個比較子。
下面我們來看一下優先隊列的建構函式
private static final int DEFAULT_INITIAL_CAPACITY = 11; private final Comparator<? super E> comparator; public PriorityQueue() { this(DEFAULT_INITIAL_CAPACITY, null); } 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; }
在空構造器初始化時,數組隊列的大小為11,比較子為null。這樣的話,放入優先隊列的對象要實現comparable介面,也可以判斷隊列中不能存放null元素。
現在我們來看一下往隊列中添加一個對象的過程。
public boolean add(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; if (i == 0) queue[0] = e; else siftUp(i, e); return true; }
可以看到,優先隊列中不可能存在null元素,添加元素首先把隊列修改次數++,判斷是否超過內部數組的長度,超過後增加數組的長度 grow(i+1)
private void grow(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError();int oldCapacity = queue.length; // Double size if small; else grow by 50% int newCapacity = ((oldCapacity < 64)? ((oldCapacity + 1) * 2): ((oldCapacity / 2) * 3)); if (newCapacity < 0) // overflow newCapacity = Integer.MAX_VALUE; if (newCapacity < minCapacity) newCapacity = minCapacity; queue = Arrays.copyOf(queue, newCapacity); }如果數組長度小於64 是按照一倍長度增長的,長度超過64,每次增長原來的百分之50,如果長度超過了int的最大值,那就設定為
Integer.MAX_VALUE,如果新的長度小於需要增長的長度,就是設定成這個長度,最後複製原來的對象到新的數組。
這樣數組長度擴充了,設定size+1。如果是空的優先隊列,就把新元素添加到隊列頭部。如果不是空,那就根據compareTo來判斷新加入的元素的優先順序別,那麼我們來看一下siftUp方法
private void siftUp(int k, E x) { if (comparator != null) siftUpUsingComparator(k, x); else siftUpComparable(k, x); }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; } 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; }
如果在建立隊列的時候,指定了比較工具,那麼就用比較子,比較子內部實現,可以根據自己的定義的比較原則,也可以調用排入佇列的元素的compareTo方法(如果實現的話),比較方法變化自行定義。
我們主要看一下沒有比較子的排序方法siftUpComparable,如下圖
假設數字越小,優先順序別越高,K的位置是新插入元素將要放置的位置,加入插入的是 25,那麼此元素的優先順序別小於它的父節點的優先順序,直接把25放在K的位置。假如放置的是8,優先順序別最高,所以 (k-1)>>>1 擷取父節點,與父節點比較,發現優先順序大於父節點,把父節點的值放入K, 把K的位置設定成父節點的下標值,遞迴查詢父節點,定位出K的位置,把新元素放入此位置。此種二叉樹演算法大大降低了演算法複雜度。
接下來我們看一下取走隊列頭部元素
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; }private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); } 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; } private void siftDownUsingComparator(int k, E x) { int half = size >>> 1; while (k < half) { int child = (k << 1) + 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; } queue[k] = x; }
如果隊列之有一個元素,就彈出這個元素,設定--size的位置為null。如果不是一個元素,彈出第一個元素,擷取數組最後的一個元素,開始調用siftDown(0, x);我們先來看一下圖解
這樣到poll掉隊列頭部元素後,隊列長度減一,所以最後的那個元素32的位置需要設定成null,這樣一來,需要填補隊列的頭,由於子節點一定小於等於父節點的優先順序,擷取父節點的子節點
int child = (k << 1) + 1; // 擷取左子節點的下標
Object c = queue[child]; // 擷取左子節點
int right = child + 1; // 擷取右子節點的下標
但右子節點不一定存在所以下面做了一個右節點不會為null的判斷,不為null的情況下,擷取兩個節點中優先順序高的一個
if (right < size &&((Comparable<? super E>) c).compareTo((E) queue[right]) > 0){
c = queue[child = right];
}
//而下面這段代碼的意思是可能出現如下圖的情況
if (key.compareTo((E) c) <= 0){
break;
}
當21移到隊列頭部時,隊列尾部的 32元素的優先順序 大於68和67的優先順序,可以直接把32元素放入到21的位置,直接結束迴圈,減少了下面空出元素的位置的重新排序演算法。
用while(k<half) 因為k如果>=half 的話,此下標以下的節點無子節點,完全二叉樹定義,也可以推到。
優先隊列 也就這兩個比較重要的方法,對於remove(Object o)方法,無非是刪除中間節點後的一種先向下排序,再向上排序的過程,可以自行查看。
歡迎交流。