Queue佇列API與原始碼分析優先順序佇列PriorityQueue實現原理
阿新 • • 發佈:2019-01-22
程式碼@1,擴容,比較簡單,下面直接將程式碼copy出來,瀏覽一下即可:public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1); //@1 容量擴容 size = i + 1; if (i == 0) queue[0] = e; else siftUp(i, e); // @2 return true; }
/** * Increases the capacity of the array. * * @param minCapacity the desired minimum capacity */ 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); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
重點分析一下siftUpUsingComparator方法,比較器不為空 首先,引數k的值為增加元素之前的size值,也就是,PriorityQueue總是在先將節點放在內部陣列元素的索引為size的位置,size加一,然後找到 element[size]的父節點,對比兩者之間的大小關係,如果小於父節點,則交換兩者的位置,繼續像上找其父節點。直到父節點為空,索引<=0 程式碼@2,上文中也說過,PriorityQueue用陣列來儲存一棵完成二叉樹,索引為n的左右子節點的索引分別為2n+1,2n+2( 2(n+1)),那已知子節點的索引為k,父節點的索引則為 看 (k -1) / 2 ,所以就不難理解 (k-1)>>>1。parent為父節點的索引。 程式碼@3,如果子節點的值大於父節點的值,由於滿足最小堆的定義(父節點比兩個子節點的值都要小),不需要調整樹的結構,直接將元素x新增到陣列索引代表的k位置。 程式碼@4,如果子節點的值小於父節點的值,則將父節點的值存入到k位置,設定k為parent,如果k的值小於等於0,則執行@6,否則繼續找parent的父節點,再次比較父節點與x的大小。重點關注@2,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) { // @1 int parent = (k - 1) >>> 1; //@2 Object e = queue[parent]; if (comparator.compare(x, (E) e) >= 0) //@3 break; queue[k] = e; // @4 k = parent; //@5 } queue[k] = x; //@6 }
2.2 public E poll() 方法詳解
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0]; // @1
E x = (E) queue[s]; // @2
queue[s] = null; //@3
if (s != 0)
siftDown(0, x);
return result;
}
再重複一下poll方法的語義,如果佇列為空,則返回null,否則移除佇列中的第一個元素,並返回。
程式碼@1,返回佇列中第一個元素,最小堆中,queue[0]代表根節點。
程式碼@2,獲取陣列中最後一個元素,該陣列有個特點,如果索引size-1的元素肯定不為空,並且為該陣列最後一個不為空的元素。陣列中的元素是連續不為空,因為從演算法角度來說,在新增元素的時候,總是首先在 queue[size-1]的位置新增元素,然後與該位置的父節點去比較,並且總是先新增左節點,然後新增右節點。從而保證陣列元素是連續的;從完成二叉樹的定義中也規定,只有最後兩層的節點的度少於二,也保證了陣列元素不會出現不連續,所謂的不連續是不允許
queue[0],queue[2]不為空,但queue[1]為空的情況。
程式碼@3,將queue最後一個節點設定為空,然後用該節點與根節點去做比較,將該樹中最小值設定為新的根節點,達到將舊根節點移除的目的。具體請看如下程式碼。
/**
* Inserts item x at position k, maintaining heap invariant by
* demoting x down the tree repeatedly until it is less than or
* equal to its children or is a leaf.
*
* @param k the position to fill
* @param x the item to insert
*/
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; // @1
while (k < half) { //@2
int child = (k << 1) + 1; //@3
Object c = queue[child]; //@4
int right = child + 1; //@5
if (right < size && //@6
comparator.compare((E) c, (E) queue[right]) > 0) //@7
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0) //@8
break;
queue[k] = c;
k = child;
}
queue[k] = x; //@9
}
程式碼siftDownUsingComparator是從根節點開始重構該樹,使之維持最下堆特性。入參中的k為0,x為原先最後一個元素。該方法的目的是要移除頭部節點,然後找到最新的頭節點。實現方法是,用指定的位置k節點開始下沉,找到最小節點成為新的頭部節點,如果k節點沒有子節點,直接將位置k的節點設定為x,原k節點的資料被刪除;如果k有子節點,然後找到k的兩個子節點中最小的那個節點位置(child為最小的節點),如果K的最小的子節點比x的大的話,說明x符合成為K兩個子節點的父節點,故直接將x替換原來的k即可,否則將最小的子節點替換K,然後從k=child,重複上述操作。
程式碼@1,因為最後一個元素為size-1,根據該陣列的特性,0-size/2的節點有子節點,不包括size/2,[0,half)為所有的根節點。故程式碼@2的迴圈條件為k>half,
程式碼@3,k的左孩子的索引
程式碼@4,k的左孩子節點的值
程式碼@5,k的右孩子
程式碼@5,@7,如果右孩子不為空,則取,左右兩個孩子中比較小的值,用來比較。
程式碼@7,如果x的值比待替換的根節點的兩個孩子節點都小,直接將X替換為根節點即可,執行程式碼@9。
否則,用根節點下兩個子節點中最小值替換一下根節點。然後從child位置重複上面的計算,確保新的子數也符合最小堆的定義,while迴圈結束有如下兩種情況,1:k>=half,說明位置k已經是葉子節點了,故需要跳出執行;如果x的值比父節點的兩個子節點都小,跳出迴圈。將下標k處的值設定為x。
2.3 public boolean remove(Object o)
public boolean remove(Object o) {
int i = indexOf(o);
if (i == -1)
return false;
else {
removeAt(i); // @1
return true;
}
}
private E removeAt(int i) {
assert i >= 0 && i < size;
modCount++;
int s = --size;
if (s == i) // removed last element
queue[i] = null;
else { // @2
E moved = (E) queue[s]; //@3
queue[s] = null; //@4
siftDown(i, moved); //@5
if (queue[i] == moved) { //@6
siftUp(i, moved); //@7
if (queue[i] != moved)
return moved;
}
}
return null;
}
該方法的實現思想是首先在queue陣列中找到該值,如果找不到,直接返回-1,是佇列最後一個元素,直接從佇列中刪除即可,如果是佇列中間的元素,刪除操作就沒那麼簡單,刪除元素後,要保證陣列元素的連續性(其實就是要保證刪除後的樹滿足最小堆的定義)
程式碼@3、@4,先用臨時變數moved儲存佇列的最後一個元素,然後將佇列最後一個元素刪除(設定為空)
程式碼@5,刪除下標為i的元素,然後用moved元素來填充一個空位。
siftDown執行完畢後,moved元素所在的位置有如下幾種可能:
1、moved放在下標為i的地方,這裡包含兩種情況,1)位置為i的元素為葉子節點。2)如果move元素比i的兩個
子元素小。
2、moved放在下標大於i的地方,如果moved元素比i的最小的子節點大,此時用最小的子節點替換父節點。i的 索引設定為原i最小節點的索引,從該索引繼續執行siftDown過程。
如果moved放在下標大於i的地方,本次刪除成功結束。
如果moved位置放在i的位置,此時,需要上浮,確定moved是否比父節點大,如果比父節點小,則不符合最小 堆定義,通過上浮調整。siftUp方法不會刪除元素,但siftDown方法會刪除一個元素。
接下來,就是從待移除的位置 i 處開始下沉,我們知道 siftDown(int k,E x)方法,可以說就是移除k位置的元素,然後將加入到以k為父節點的樹中(保持最小堆特性),但要是位置為k的地方是葉子節點呢?siftDown的做法是直接將x放入到k的位置。但這樣會不會影響最小堆特性呢,答案是可能的,也就有了程式碼@6的判斷,如果queue[i]
== moved,說明此時k是葉子節點,,應該從索引為k向上升,判斷該節點與父節點的關係,所以呼叫siftUp(int i, E x)方法,上浮,確保維持最小堆特性。在這裡,我還想再囉嗦一下,重溫一下siftUp的實現邏輯:如果i<=0,直接將x放入到佇列索引為i的位置。如何大於0,則找到該節點的父節點,然後比較父節點與x的大小關係,如果x大於根節點,則直接將x放入到下標為i的地方,否則,需要將父節點放入位置i上,然後繼續k=parent。
提問:
PriorityQueue removeAt 返回值,是被移除的元素嗎?
2.4 public Iterator<E> iterator()
迭代器
首先,迭代器,既然PriorityQueue內部是一個數組queue[],那迭代器,直接遍歷該陣列就好了,是的,但迭代器與for(int i =0; i < size; i++)這中遍歷方法,還有一個特殊的是,迭代器支援將當前迭代的元素刪除,也就是remove方法。我們也知道,移除一個元素後,要重新調整結構,保證刪除一元素後繼續保持最小堆的特性。從上文中我們知道,從0-size-1直接刪除一個元素,通常的做法,是先將尾部節點(移除佇列,將size減一,然後將queue[size]的元素用臨時變數儲存,然後將queue[size]=null),用該節點與要刪除的元素進行對比,替換,再重構樹特性。
所以,Itr類的設計,就是基於上述的考慮。
private final class Itr implements Iterator<E> {
/**
* Index (into queue array) of element to be returned by
* subsequent call to next.
*/
private int cursor = 0; //@1
/**
* Index of element returned by most recent call to next,
* unless that element came from the forgetMeNot list.
* Set to -1 if element is deleted by a call to remove.
*/
private int lastRet = -1; //@2
/**
* A queue of elements that were moved from the unvisited portion of
* the heap into the visited portion as a result of "unlucky" element
* removals during the iteration. (Unlucky element removals are those
* that require a siftup instead of a siftdown.) We must visit all of
* the elements in this list to complete the iteration. We do this
* after we've completed the "normal" iteration.
*
* We expect that most iterations, even those involving removals,
* will not need to store elements in this field.
*/
private ArrayDeque<E> forgetMeNot = null; //@3
/**
* Element returned by the most recent call to next iff that
* element was drawn from the forgetMeNot list.
*/
private E lastRetElt = null; //@4
/**
* The modCount value that the iterator believes that the backing
* Queue should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
private int expectedModCount = modCount;
public boolean hasNext() { //@5
return cursor < size ||
(forgetMeNot != null && !forgetMeNot.isEmpty());
}
public E next() {
if (expectedModCount != modCount)
throw new ConcurrentModificationException();
if (cursor < size)
return (E) queue[lastRet = cursor++];
if (forgetMeNot != null) {
lastRet = -1;
lastRetElt = forgetMeNot.poll();
if (lastRetElt != null)
return lastRetElt;
}
throw new NoSuchElementException();
}
public void remove() {
if (expectedModCount != modCount)
throw new ConcurrentModificationException();
if (lastRet != -1) {
E moved = PriorityQueue.this.removeAt(lastRet);
lastRet = -1;
if (moved == null)
cursor--;
else {
if (forgetMeNot == null)
forgetMeNot = new ArrayDeque<>();
forgetMeNot.add(moved);
}
} else if (lastRetElt != null) {
PriorityQueue.this.removeEq(lastRetElt);
lastRetElt = null;
} else {
throw new IllegalStateException();
}
expectedModCount = modCount;
}
}
程式碼@1,cursor 遍歷陣列的遊標,從0開始, 從這裡也可以看出,在沒有呼叫it.remove方法時,就是遍歷整個陣列元素。
程式碼@2,lastRet 上一次返回的元素索引。-1代表未返回。
程式碼@3,forgetMeNot 這個佇列由什麼用呢?大家可以先想想。
程式碼@4,lastRetElt 上一次返回的元素。
程式碼@5,判斷是否還有可遍歷的元素,如果cursor 小於size,或者forgetMeNot 不為空,說明有元素可遍歷,這裡forgetMeNot不為空,為什麼就會有元素可遍歷呢?其實將目光放入到remove方法時,會發現,forgetMeNot中的元素,其實就是removeAt返回的元素,那我們來探究removeAt(int
i)在什麼情況下會返回不為空的元素,PriorityQueue在移除下標為i的元素,是這樣處理的,首先,將佇列的size-1,然後將最後一個元素出佇列,記為moved,然後去執行 siftDown( i , moved),此方法會首先肯定能刪除原先位置為i的元素,但moved會放在佇列的什麼地方呢?因為moved存入的位置,直接會影響到@1中cursor遊標去順序訪問陣列相關。如果moved最後放在大於i的位置,則可以直接返回null,否則需要返回,並放入一個臨時地方,保證最後能遍歷到。所以,forgetMeNot就是用來儲存未遍歷到,但已經移動到小於或等於cursor的位置的元素。
在此,特意感謝 http://shmilyaw-hotmail-com.iteye.com/blog/1775868 該篇文章詳細講解了最小堆的實現原理,讓我受益匪淺。