一.優先隊列的應用
優先隊列在程序開發中屢見不鮮,比如操作系統在進行進程調度時一種可行的算法是使用優先隊列,當一個新的進程被fork()出來后,首先將它放到隊列的最后,而操作系統內部的Scheduler負責不斷地從這個優先隊列中取出優先級較高的進程執行;爬蟲系統在執行時往往也需要從一個優先級隊列中循環取出高優先級任務并進行抓取。可以想見,如果類似這樣的任務不適用優先級進行劃分的話,系統必會出現故障,例如操作系統中低優先級進程持續占用資源而高優先級進程始終在隊列中等待。此外,優先隊列在貪婪算法中也有一些應用。
二.優先隊列的實現原理
優先隊列的實現方式是使用二叉堆的結構,需要滿足以下兩條性質(Heap property),這里以小頂堆為例講解:
1.任何結點的值都小于或等于其子節點的值。
2.所有結點從上到下,從左到右填入,即一棵完全二叉樹。
基于這兩條規律,二叉堆在實現中往往會使用一個數組,下面我們研究一下JDK中二叉堆(優先隊列)的實現。
三.優先隊列在JDK中的實現方式
研究源碼最好的方式是debug,看每一步變量的變化,我們可以簡單寫一個Demo,debug進源碼一探究竟:
這里我們簡單地創建一個優先隊列,向其中添加三個元素,我們可以在代碼第一行打一個斷點,如果您使用Eclipse編輯器的話,接下來可以按F5進入源碼中:
代碼運行到這里,PriorityQueue調用自己的一個重載構造器,第一個參數是數組默認大小,第二個是元素比較的Comparator,我們這里的Demo比較簡單,您在使用優先隊列時可以選擇實現自己的Comparator。
1
2
3
4
5
6
7
8
9
|
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; } |
接下來我們研究一下添加元素時的offer操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 ) //第一次添加元素,直接放到第0個位置即可 queue[ 0 ] = e; else //需要將元素放到最后,再做上濾操作 siftUp(i, e); return true ; } |
我們逐行來解釋一下,首先offer方法判斷參數是否為空,不為空則對變量modCount自增,modCount記錄了隊列被修改的次數,接下來,判斷數組是否會越界,如果越界則通過grow進行擴容,接下來添加元素,如果當前元素為0個則直接將元素放到數組第一個位置,否則做一個siftUp的操作。
1
2
3
4
5
6
7
8
9
10
11
|
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); |
上面的代碼對隊列擴容,源碼中注釋也很清晰,首先判斷當前的數組大小是否足夠小(<64),如果足夠小則將大小擴充為2倍,否則將原大小加上50%。需要注意的是,這里最后做了一個大小是否溢出的判斷。
1
2
3
4
5
6
7
|
private static int hugeCapacity( int minCapacity) { if (minCapacity < 0 ) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } |
如果需要擴容的大小已經<0了,顯然已經溢出了,在這里拋出了OutOfMemory的異常。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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; } |
為了保證優先隊列的性質1,在插入每個元素時都需要與該節點父親進行比較,找到其正確位置,有些數據結構書中,這個操作被稱為上濾(percolate up)。
入隊操作已經說完了,接下來是出隊操作,即poll()操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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; } |
這個方法首先將數組第一個元素作為結果,(因為如果是小頂堆的話堆頂始終是最小元素),并將隊列的最后一個元素放到第一個位置,最后用siftDown做一些調整,保證隊列的性質,這個操作被稱為下濾(percolate down)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private void siftDownUsingComparator( int k, E x) { int half = size >>> 1 ; //這里k必須有孩子,故葉節點需要比較 while (k < half) { //以下幾行代碼到較小的那個兒子,用變量c表示 int child = (k << 1 ) + 1 ; //這里假設左兒子比較小 Object c = queue[child]; int right = child + 1 ; //左右兒子比較,如果右兒子小則將c賦值為右兒子 if (right < size && comparator.compare((E) c, (E) queue[right]) > 0 ) c = queue[child = right]; //如果x比小兒子還小,說明k就是正確位置 if (comparator.compare(x, (E) c) <= 0 ) break ; queue[k] = c; k = child; } queue[k] = x; } |
如上圖,下濾過程中k不斷與其兒子進行比較,如果滿足優先隊列的順序性,則break出循環。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://www.cnblogs.com/showing/p/6759341.html