優先隊列探究
隊列的特點是先進先出。通常都把隊列比喻成排隊買東西,大家都很守秩序,先排隊的人就先買東西。
但是優先隊列有所不同,它不遵循先進先出的規則,而是根據隊列中元素的優先權,優先權最大的先被取出。通常把優先隊列比喻成現實生活中的打印。一個打印店裏有很多打印機,每臺機器的性能不一樣,有的打印機打印很快,有的打印機打印速度很慢。當這些打印機陸陸續續打印完自己的任務時進入排隊等候狀態。如果我這個時候要打印一份文件,我選的不是第一個排隊的打印機,而是性能最好,打印最快的打印機。
優先隊列的特點就是將存儲的元素根據其優先權的高低,先取出優先權高的元素。所以,優先隊列的實現方法有很多。
如果優先權的範圍是已知的,那麼就可以嘗試用一個二維數組來實現優先隊列。每一行表示一個優先級別。例如用大小爲a[10][10]的二維數組來實現一個優先隊列,a[0]表示一個優先級別,裏面存放優先級爲0的元素,a[10]則存放優先級最高的元素。這樣根據元素的優先級進行存儲,取出元素的時候,根據優先級,先取出優先級最高的元素。
上面的方法在優先權範圍已知且比較集中可以估計的情況下可以適用,但是如果優先權的範圍不清楚,或者間隔很大,就不再使用。實現優先隊列也可以用一個鏈表隊列加以實現。鏈表的節點數據包含兩個部分,隊列的數據項和該數據項的優先權。將元素存入鏈表,需要用時,遍歷鏈表,查找優先權最大的數據項。
還可以用下面的方法。用一個數組來存放優先權,另外一個相應的數組存放要存放的元素。由於,不同的元素可能會有相同的優先權。所以,存放元素的數組中不是直接存放元素,而是存放鏈表,就像用掛鏈法來解決哈希衝突一樣。當有相同優先權的元素不止一個時,則掛在相應數組索引位置的鏈表上,如圖1。根據優先權數組中存放的優先權,來取得優先權最大的元素。由於數組查找比鏈表要快,所以,這個方法比上面的方法的改進。
用Java來實現上述的優先隊列:
四個私有屬性:
private int[] priority
private ElementNode[] elements
private int highest
private int manyItems
private int PriotityCount
priority數組是用來存放優先權的,初始的值都是0,之後賦予的優先權值必須都大 於0。
Elements是用來存放元素鏈表的數組。初始值都是null,然後往裏面掛上鏈表。
highest元素是記錄優先權最大的元素的數組索引位置,這樣,可以方便使用,提高效 率。因爲每次要查找優先權最大的元素時,不需要再重新查找。
manyItems用來記錄隊列中已經存放的元素個數。
PriotityCount用來記錄不同優先權的數目。因爲有鏈表的存在,manyItems不能衡量 數組是否滿了,而PriotityCount對應的是優先權數組中的使用優先權個數,可以衡量數 組使用情況。
兩個私有方法:
private int getHighest(){}
private int contains(int priority){}
private int getHighest(){}方法是用來取得隊列中優先權最大的元素的數組索引位 置。
private int contains(int priority){}方法是用來查找隊列中是否存在指定優先 權。如果存在,返回優先權所在的數組索引位置,如果不存在,則返回-1。
三個公有方法:
public Object getFront(){}
public void insert(Object item, int priority){}
public Object removeFront(){}
public Object getFront(){}方法是用來取得優先權最大的元素的。
public void insert(Object item, int priority){}方法是用來往隊列中添加元素 的。衡量優先權的標準可以很多,這裏是直接要求用戶在存儲元素的時候存儲優先權。這個 隊列也還沒有考慮隊列滿了以後擴充的情況,所以如果滿了會報錯。在插入元素時,先要查 看這個元素所對應的優先權是否已經存在,如果已經存在了,那就直接掛到相應數組索引位 置的鏈表下面。如果不存在,則存放在一個還沒有存放值的數組位置中。如果加入元素成 功,要檢查是否比原來最大的優先權還要大,如果是,則把highest標記爲這個新插入元素 的索引位置。
public Object removeFront(){}方法是用來刪除優先權最大的元素的。刪除成功時, 會返回被刪除的元素值,否則返回null。刪除優先權最大的元素後,還要查找出新的優先 權最大的值,並且讓highest指向這個值的索引位置。
示例代碼:
package cn.priorityQueue;
/**
* 這個優先隊列是基於數組的。
* 實現方法是,用兩個數組,一個存放元素的權限,另外一個存放元素的值,兩個數組的位置相互對應。
* 往這個隊列中存儲元素,需要放入兩個值,一個是元素的優先級值,另外一個是元素的值
* @author William Job
*
*/
public class PriorityQueue01 {
//這個數組priority用來表示每個元素的優先權
private int[] priority;
//這個數組element用來存儲每個元素的值
private ElementNode[] elements;
//這個值highest用來指向最高優先權的元素
private int highest = 0;
//manyItems用來計數已經存儲的元素個數
private int manyItems;
//PriorityCount用來計數已經存儲的優先權個數
private int PriotityCount;
public PriorityQueue01(int initialCapacity){
priority = new int[initialCapacity];
elements = new ElementNode[initialCapacity];
manyItems = 0;
PriotityCount = 0;
}
/**
* 這個方法是用來取得優先權最大的值的。
* @throws IllegalAccessException
* @return:返回擁有最大優先權的值
*/
public Object getFront(){
if(manyItems == 0){
throw new IllegalArgumentException("沒有元素!");
}
return elements[highest].element;
}
/**
* 這個方法用來向隊列中添加一個元素
* 在這個優先隊列的實現中,必須同時給定元素的值和元素的優先值
* @param item:元素的值
* @param priority:元素的優先值,必須大於0
* @throws Exception
*/
public void insert(Object item, int priority){
if(PriotityCount >= this.priority.length){
throw new IllegalArgumentException("隊列已滿");
}
if(priority <= 0){
throw new IllegalArgumentException("優先權必須大於0!");
}
int add = contains(priority);
if(add >= 0){
ElementNode node = new ElementNode();
node.element = item;
node.link = elements[add];
elements[add] = node;
manyItems ++;
}
else{
ElementNode node = new ElementNode();
node.element = item;
if(this.priority[PriotityCount] == 0){
elements[PriotityCount] = node;
add = PriotityCount;
}
else{
for(int j = 0; j < this.priority.length; j ++){
if(this.priority[j] == 0){
add = j;
}
}
elements[add] = node;
}
this.priority[add] = priority;
manyItems ++;
PriotityCount ++;
}
if(this.priority[add] > this.priority[highest]){
highest = add;
}
}
/**
* 這個方法是用來取得隊列中優先權最大的元素的
* @return:返回優先權最大的元素的索引位置
*/
private int getHighest(){
int index = -1;
int temp = 0;
for(int i = 0; i < priority.length; i ++){
if(priority[i] > temp){
temp = priority[i];
index = i;
}
}
return index;
}
/**
* 這個方法用來查找隊列中是否已經存在這個優先權
* @param priority:要查找的優先權
* @return:如果這個優先權已經存在則返回這個優先權相應的索引位置,如果不存在則返回-1
*/
private int contains(int priority){
int index = -1;
for(int i = 0; i < PriotityCount; i ++){
if(this.priority[i] == priority){
index = i;
}
}
return index;
}
/**
* 這個方法用來刪除優先權最大的元素,同時返回這個元素。
* 當刪除這個元素後,如果這個索引位置再也沒有相應的元素,則這個索引位置的優先權賦值爲0
* @return:隊列中優先權最大的元素
*/
public Object removeFront(){
ElementNode temp = elements[highest];
if(elements[highest].link != null){
elements[highest] = elements[highest].link;
}
else{
elements[highest] = null;
priority[highest] = 0;
PriotityCount --;
}
highest = getHighest();
manyItems --;
return temp.element;
}
}
package cn.priorityQueue;
/**
* 這個是元素的節點類。
* 這個類中有兩個屬性:
* element存放元素的值,link指向下一個節點
* @author William Job
*
*/
public class ElementNode {
//元素的值
public Object element;
//指向下一個元素的地址
public ElementNode link;
public Object getElement() {
return element;
}
public void setElement(Object element) {
this.element = element;
}
public Object getLink() {
return link;
}
public void setLink(ElementNode link) {
this.link = link;
}
}
JDK實現的優先隊列PriorityQueue研究
JDK中的java.util.PriorityQueue實現方法不同於上面這些方法,它是基於堆的。
堆本質是一棵二叉樹,其中所有的元素都可以按全序語義(參見附錄說明)進行比較。用 堆來進行存儲需要符合以下規則:
1.數據集中的元素可以用全序語義進行比較;
2.每個節點的元素必須大於或小於該節點的孩子節點的元素;
3.堆是一棵完全二叉樹。
用堆來實現優先隊列,跟節點始終是優先權最大的元素節點。
插入的思路是這樣的,當插入一個元素時。先將這個元素插入到隊列尾,然後將這個新插入的元素和它的父節點進行優先權的比較,如果比父節點的優先權要大,則和父節點呼喚位置,然後再和新的父節比較,直到比新的父節點優先權小爲止,參見圖2和圖3
這個尋找新插入元素位置的過程對應於java.util.PriorityQueue源代碼中的
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;
}
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;
}
咦?不是二叉樹結構嗎?不是節點嗎,爲什麼這裏看到的是操作數組?我原來有這樣的 疑問,因爲之前自己實現過的二叉樹只是止於鏈式結構。然而,樹存在如下特點:
1.根節點的數據總是在數組的位置[0]
2.假設一個非跟節點的數據在數組中的位置[i],那麼它的父節點總是在位置[(i-1)/2]
3.假設一個節點的數據在數組中的位置爲[i],那麼它的孩子(如果有)總是在下面的這兩個位置:
左孩子在[2*i+1]
右孩子在[2*i+2]
基於這些特點,用數組來表示數會更加方便。源代碼中int parent = (k - 1) >>> 1等價於int parent = (k - 1) / 2;對於這裏涉及到的位運算,如果有不太明白的, 可以參見附錄文檔。
從優先隊列中刪除優先權最大的元素的思路是將隊列尾的元素值賦給跟節點,隊列爲賦 值爲null。然後檢查新的根節點的元素優先權是否比左右子節點的元素的優先權大,如果 比左右子節點的元素的優先權小,就交換位置,重複這個過程,直到秩序正常。參見圖4和 圖5
這個刪除根節點後,重新恢復合理順序過程對應於源代碼中
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;
}
關於優先隊列的探究目前進行到這裏,和大家探討,裏面還有很多內容可以深入探究。
附:
全序語義:
一個類的全序語義要求定義6種比較操作符(==、!=、>=、<=、>、<),以形成符合以下要求的全序: 1. 等同性:當且僅當x和y的值相同時,(x == y)爲真。 2.完全性:對於任何兩個值x和y,下面三種比較中總有一個爲真: (x < y)、(x == y)或(x . y)。 3. 一致性:對於任何兩個值x和y: (x>y)與(y<x)一致 (x>=y)與((x>y)||(x==y))一致 (x<=y)與((x<y)||(x==y))一致 (x!=y)與!(x==y)一致 4.傳遞性:對於任何三個值(x,y和z),如果(x<y)且(y<z),那麼(x<z)
|
關於位運算,這裏有兩篇材料介紹地非常不錯,可以參考:
http://flowercat.iteye.com/blog/380859
http://www.blogjava.net/rosen/archive/2005/08/12/9955.html
http://topic.csdn.net/u/20080626/20/59a05c26-acb3-4d74-a153- 711ce3a664ff.html
其餘參考資料:
數據結構Java語言描述 【美】Michael Main著 孔芳 周麗琴譯
數據結構與算法C#語言描述 【美】Milchael McMillan著 呂秀鋒 崔睿譯
Java核心技術卷II
JDK API文檔