這裏介紹一種重要的略爲複雜的數據結構“優先隊列”.我所提供的不是教學,而是希望提供一個解決類似問題的思路.因此以學習爲目的,而不是以實用, 而且涉及到較多的名詞概念或術語,對於不懂的建議查找數據結構的書籍,這裏考慮到文章的篇幅就不多做介紹. ^^
優先隊列(priority queue)是一個以集合爲基礎的抽象數據類型,每個元素都有一個優先級,對優先隊列執行的操作有1) 查找;2) 插入一個新元素;3) 刪除。在最小優先隊列(min priority queue)中,查找操作用來搜索優先權最小的元素,刪除操作用來刪除該元素;對於最大優先隊列(max priority queue),查找操作用來搜索優先權最大的元素,刪除操作用來刪除該元素。優先權隊列中的元素可以有相同的優先權,查找與刪除操作可根據任意優先權進行。
優先隊列的數據結構實現方式有很多種: 無序鏈表,有序鏈表,二叉搜索樹,左高樹,優先級樹等等.其中優先級樹滿足以下兩個性質:
1)樹中每個結點只存一個元素
2)樹中任意一個結點的值高於其兒子結點中存儲的元素的值.
特別的,當一棵優先級樹爲近似滿二叉樹時,我們稱它爲堆或偏序樹.比如堆排序就是利用這種數據結構(優先隊列)來完成的,優先級就是待排數據的值.
以下舉5個具體的例子說明優先隊列在解決問題中的應用.
(1) 在作業調度算法中常常利用的優先隊列數據結構,先舉”短作業優先調度”爲例子:
一般情況下,我們希望將耗時少的作業儘快完成,也就是說短作業都優先於已經消耗一些時間的作業.使短作業優先而又不讓鎖死長作業的方法之一就是爲每個作業分配一個優先級.分配優先級公式是100 * (Tused(x)-Tinit(x)),Tused()是作業總共消耗的時間,Tinit()是作業到達的時間.100是一個可以根據需要進行調整的數,通常要大等於最大的作業總數.一個作業可以用一個由作業標識符和作業優先級組成的結構體來表示,即:
struct ProcessType{
int id;
int priority;
};
爲了給作業安排時間,分時系統中設置了類型爲ProcessType的優先隊列WAITING. 過程Initial()和Select()對該優先隊列做處理.每當一個新作業到達時,過程Initial()就將這個作業插入優先隊列WAITING中.當系統有時間段可以使用時,Select()就從中選出一個優先權最高的一個作業,並將該作業從WAITING中刪除,由Select()暫存該作業記錄,以便完成時間段後帶回一個新的優先級重新入隊. 這裏假設有DeleteMin()函數返回是指向優先級最高的作業的指針,CurrentTime()是用來記時的函數,Execute()是處理作業的過程.下面給出該算法實現的框架.
void Initial(int p) //p是作業號
{
ProcessType process;
process.id = p;
process.priority = -CurrentTime();//記時
Insert(process, WAITING); //對新達到的作業入優先隊列
}
void Select()
{
int begintime, endtime;
ProcessType process;
process = (DeleteMin(WAITING))->element;//選中優先級最高的作業並出隊
begintime = CurrentTime();
Execute(process.id); //作業調入運行
endtime = CurrentTime();
process.priority += 100 * (endtime-begintime); //重新分配優先級
Insert(process, WAITING);//process帶回新的優先級重新入隊
}
<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
(2)利用優先隊列的堆結構完成排序即堆排序,可以認爲其中的優先級就是待排數據的值:
void PushDown_MinHeap(int first, int last)
{//完成構建最小堆性質的功能
long i, j, x;
i = first;
j = i * 2;
x = Data[i];
while (j <= last){
if (j < last && Data[j] < Data[j+1])//選擇左右兒子中較大者的下標
j++;
if (x < Data[j]){//壓堆,以保持最小堆性質
Data[i] = Data[j];
i = j;
j = 2 * i;
}
else
break;
}
Data[i] = x;
}
void MinHeapSort(int first, int last) //以下是抽象的堆排序算法
{//如果L是非空的表,標記*步驟可以利用到PushDown_MinHeap()直接完成初始化
for (表L中所有元素) do Insert(x,S); //*
while (not EMPTY(S)){
y = DeleteMin(S);
輸出y;
}
}/*堆排序具體實現見http://expert.csdn.net/Expert/topic/2059/2059607.xml?temp=.8768579 */
(3) 優先隊列在很多”貪心算法”中都可以得到很好的應用,這點很值得注意.例如Huffman樹的編碼規則具有”貪心選擇”的性質.假定給出優先隊列的Insert()和DeleteMin()2個基本運算,使用堆的數據結構,抽象的算法實現過程大致如下:
PriorityQueueType *Huffuman(PriorityQueueType *S)//構建霍夫曼編碼樹
{//如果L是非空的表,標記*步驟可以利用到PushDown_MinHeap()直接完成初始化
for (表L中所有元素) do Insert(x,S); //*
for (int i = 0; i < n; i++){
x = DeleteMin(S); //取兩個最小權的結點
y = DeleteMin(S);
PriorityQueueType *t = new < PriorityQueueType>;
MakeTree(x, y, t);//把結點x,y合併成以結點t爲根的樹.
t.priority = x.priority + y.priority;
Insert(t, S);//把根樹t,重新入優先隊列
}//循環結束後,優先隊列S中只剩1個結點.
return GetMin(S)//返回優先隊列S中的最小權的結點,即爲Huffman樹根
}
(4) 機器調度問題: 考察一個機械廠,其中有m 臺一模一樣的機器。現有n 個作業需要處理,設作業i 的處理時間爲ti ,這個時間爲從將作業放入機器直到從機器上取下作業的時間。所謂調度( s c h e d u l e)是指按作業在機器上的運行時間對作業進行分配,使得:
• 一臺機器在同一時間內只能處理一個作業。
• 一個作業不能同時在兩臺機器上處理。
• 作業i 一旦運行,則需要ti 個時間單位。
我們的任務是寫一個程序,以便確定如何進行調度才能使在m 臺機器上執行給定的n 個作業時所需要的處理時間最短。
調度問題是著名的NP-複雜問題(NP表示nondeterministic polynornial) 中的一種。NP-複雜及NP-完全問題是指尚未找到具有多項式時間複雜性算法的問題。這類問題通常需要採用近似算法來解決. 在調度問題中,採用了一個稱爲最長處理時間優先(longest processing time first, LPT)的簡單調度策略,它可以幫助我們獲得一個較理想的調度長度,該長度爲最優調度長度的4/3-1/(3*m)。在LPT算法中,作業按它們所需處理時間的遞減順序排列。在分配一個作業時,總是將其分配給最先變爲空閒的機器。
定理: F*(I)爲在m臺機器上執行作業集合I的最佳調度完成時間,F(I)爲採用LPT調度策略所得到的調度完成時間,則(F(I) / F*(I)) <= 4/3 – 1/(3*m).
實際上,L P T調度會比上述所給界限更接近最優解.很明顯的,LPT調度具有某種貪心(longest processing time first)的性質,據此我們可以採用優先隊列來解決.也就是對所給各作業的完成時間T[N]做按降序排序後即爲作業調度的序列.算法就是用堆結構實現優先隊列的排序,即堆排序.
(5)裝箱問題: 設有N件物品,每件物品的體積爲S1,S2,..,Sn,且0<Si<=1 (i = 1,2,...,n).現在有一批箱子,每隻現在的容量爲1個單位.現在的問題是:能夠容納這N件物品的箱子至少需要多少隻.或者說,給定正整數M,問能否把這N件物品放入此M只箱子中去?
這個問題是很有實際意義的,例如在計算機內存中分配數據,又如從一個大的原材料中剪裁各種需要尺寸的零件等.裝箱問題和裝配問題的本質是一樣的,屬於NP-複雜問題.爲了能找到裝箱問題的最優解,我們需要將n項的集合劃分成n個及少於n個的子集,但這樣劃分的總數將超過(n/2)^(n/2),因此這種方法將不是多項式界的.根據上述給出的定理,我們同樣可以構造出一個近似的貪心(NIFF-Noningressing First Fit)算法,簡稱First-Fit.其意思是每個物體一次放在它的第一個能夠放得進去得箱子中. 設V*(I)是最優解,根據上面給出的定理可以推出,由NIFF算法所產生的放在除V*(I)個箱子外中的物體體積至多爲1/3.並且還可以推出由NIFF算法得到的放在額外箱子中的物體個數至多爲V*(I)-1個.(證明略) 此外裝箱問題可以很多種近似算法,以下先介紹抽象的NIFF算法過程:
/*
B[]爲記錄放在第j只箱子的所有物品標號的集合.
以下是由大到小的裝填算法;
*/
bool BNIFF(int n, int m, PriorityQueueType *S, TSet B[])
{
float *b = new[m+1];
for (int i = 0; i <= m; i++)
b[i] = 0; //b[i]標記第i只箱子已裝的容量
物品體積S[i]按其值大小爲優先級降序整序; //提高近似精度
for (t = 0; t < n; t++){
j = 1; //從標號爲1的箱子開始搜索
while (b[j] + S[t] > 1 && j <= m)
j++; //尋找最先能容納物品S[t]的第j只箱子,優先放入該箱
if (j >m) return false; //M只箱子容納不下此N件物品
B[j] <- B[j]∪{t}; //第t件物品存放在第j只箱子中,加入集合B[j]記錄
b[j] += S[t];
}
Output(B[1], B[2], ... B[M])
return true;
}
容易證明,上述NIFF算法的時間效率爲O(N^2),爲多項式界的算法.而上面優先隊列S的數據結構僅僅是一個簡單的有序數組.
總結: 以上主要是介紹數據結構中的”優先隊列”在一些典型問題中的應用.在”優先隊列”中,優先是表示”服務”不是排隊順序進行的,而是按照每個對象的優先級順序提供服務.此種性質很適合利用在某些具有貪心選擇性質的算法中(作爲實現該算法的數據結構),這點也是敘述上面幾個問題所要說明的重心.
特別指出的是,算法是不依賴具體的計算機語言的,上面大都使用抽象的算法模型來描述解決問題的方法,也使用了抽象數據類型來做爲描述算法的工具,以上做法都是爲了方便描述算法原理而不拘泥於具體實現. 很顯然的,不同計算機語言都可以實現同個算法,而實現某種算法不但可以使用不同的抽象數據類型,更可以爲該抽象數據類型選擇合適的具體實現的手段.
最後還需要注意的是,上面的重點更側重優先隊列這種抽象數據類型在解決某類問題所使用的算法方面的應用或思路.文章中我也沒有給出優先隊列的某種具體實現代碼,因爲它的實現方式有多種,也對應於不同情況的需要效率也不盡相同,需要的話請自行查閱參考書,因爲數據結構的書會比我說的詳細,深刻的多,這裏我就不多敘述. :)