資料結構之優先佇列(線性結構的優先佇列,陣列實現和連結串列實現)
優先佇列(priority queue)
普通的佇列是一種先進先出的資料結構,元素在佇列尾追加,而從佇列頭刪除。在優先佇列中,元素被賦予優先順序。當訪問元素時,具有最高優先順序的元素最先刪除。優先佇列具有最高階先出 (first in, largest out)的行為特徵
優先佇列是0個或多個元素的集合,每個元素都有一個優先權或值,對優先佇列執行的操作有1) 查詢;2) 插入一個新元素;3) 刪除.在最小優先佇列(min priority queue)中,查詢操作用來搜尋優先權最小的元素,刪除操作用來刪除該元素;對於最大優先佇列(max priority queue),查詢操作用來搜尋優先權最大的元素,刪除操作用來刪除該元素.優先權佇列中的元素可以有相同的優先權,查詢與刪除操作可根據任意優先權進行.
優先佇列經常通過堆的方式來實現,查詢和刪除的時間複雜度都是log2n,所以很快
另一種描述方法是採用有序線性表,當元素按遞增次序排列,使用連結串列時則按遞減次序排列,這兩種描述方法的刪除時間均為( 1 ),插入操作所需時間為(n).
基礎概念就是這麼些了,因為我現在總結的是線性結構,所以我們來用線性的結構實現這個資料結構
我先來用陣列的方式來實現一下
我來引進一個問題,任務排程問題,程序i需要pri的時間,求各任務等待的時間加起來最小的排序
下面是我的測試資料檔案:
基礎概念裡面說到,資料都有優先順序,所以我們就想到了,資料就分為了兩部分:資料本身,優先權。
所以我們將它封裝成一個結構體:
struct data {
int num;
int prior;//優先權
};
接下來是優先佇列這個資料結構的結構體封裝,我們實現的是陣列的優先佇列,它和陣列佇列有一樣的性質,那麼就是下面這樣的結構:
struct prqueue {
struct data datanum[MAX];
int size;
};
建立優先佇列的函式:其實就是陣列的初始化和大小的初始化
struct prqueue* createqueue() {
struct prqueue* newqueue = (struct prqueue*)malloc(sizeof(struct prqueue));
newqueue->size = 0;
memset(newqueue->datanum, 0, sizeof(data) * MAX);
return newqueue;
}
接下來是查詢佇列是否為空:
bool isempty(struct prqueue* pqueue) {
return pqueue->size == 0;
}
接下來是入隊操作,也就是陣列佇列的常規入隊:
void push(struct prqueue* pqueue, struct data pdata) {
if (pqueue->size + 1 == MAX) {
printf("隊滿無法入隊");
return;
}//陣列是要判斷一下隊滿的情況的
pqueue->datanum[pqueue->size].num = pdata.num;
pqueue->datanum[pqueue->size].prior = pdata.prior;
pqueue->size++;
}
出隊的操作,刪除和寫入變數的函式,我們利用傳入一級指標來讀取資料:
優先隊列出隊出去的是優先順序最高的數,我這裡權值最小的就是我們所需要的優先順序最高的
所以就是找到最小的權值的資料:
void pop(struct prqueue* pqueue, struct data* pdata) {
if (isempty(pqueue)) {
printf("隊為空,無法出隊");
return;
}
int minindex = 0;
struct data mind = pqueue->datanum[0];
for (int i = 1; i < pqueue->size; i++) {
if (mind.prior > pqueue->datanum[i].prior) {
mind = pqueue->datanum[i];
minindex = i;
}
}//選擇排序,找到優先順序最小的
pdata->num = mind.num;
pdata->prior = mind.prior;
for (int i = minindex; i < pqueue->size; i++) {
pqueue->datanum[i] = pqueue->datanum[i + 1];
}//偽刪除,所刪除的元素後面的陣列集體往前移動
pqueue->size--;
}
接下來是主函式來實現上面的問題排序
int main() {
struct prqueue* pqueue = createqueue();
struct data readdata;
FILE* fp = fopen("test.txt", "r");
if (fp == NULL) {
printf("無法開啟資料夾");
return 0;
}
while (fscanf(fp, "%d %d\n", &readdata.num, &readdata.prior) != EOF){
push(pqueue, readdata);
}
fclose(fp);
int workindex = 1;
while (!isempty(pqueue)) {
pop(pqueue, &readdata);
printf("\t%d\t%d\t%d\n", workindex, readdata.num, readdata.prior);
workindex++;
}
return 0;
}
以下是結果:
我上面提到,線性結構可以實現優先佇列,那麼除了陣列,是不是還有一種結構?
對,連結串列,讓我們來看一下,用連結串列來實現這個問題的優先佇列
首先就是節點和優先佇列的連結串列結構體實現和建立:
我用的是雙向連結串列,因為這樣在插入的時候根據優先順序自動選擇插入更加方便(但是隨之而來容易出現異常和bug)
struct node {
struct node* front;
struct data ndata;
struct node* next;
};//雙向連結串列的節點的結構體
struct listpriqueue {
struct node* headnode;
struct node* tailnode;
int size;
};//鏈式優先佇列的結構體
bool isempty(struct listpriqueue* plist) {
return plist->size == 0;
}//判斷是否為空的萬金油函式
struct node* createnode(struct data pdata) {
struct node* newnode = (struct node*)malloc(sizeof(struct node));
newnode->next = newnode->front = NULL;
newnode->ndata = pdata;
return newnode;
}//建立新節點,初始化新節點
struct listpriqueue* creatlistpriqueue() {
struct listpriqueue* newqueue = (struct listpriqueue*)malloc(sizeof(struct listpriqueue));
newqueue->headnode = newqueue->tailnode=NULL;
newqueue->size = 0;
return newqueue;
}//初始化新鏈式優先佇列並建立返回
接下來是重頭戲,就是鏈式優先佇列的插入,依照優先順序插入,也就是雙向連結串列的索引插入法,這裡只不過運用了比較的方式找到合適點插入:
void push(struct listpriqueue* plist, struct data pdata) {
struct node* newnode = createnode(pdata);
if (isempty(plist)) {
plist->headnode = plist->tailnode = newnode;
}
//這裡一定要分好情況,要不然各種指標指向NULL,或者是出不來資料,死迴圈問題,小心,一定要理解我下面的東西
else {
struct node* pmove = plist->headnode;
while (pmove != plist->tailnode) {
if (newnode->ndata.prior < pmove->ndata.prior && pmove == plist->headnode) {//當比頭節點還小,也就是,要插入的資料優先順序最大的時候,就是頭節點插入法
newnode->next = plist->headnode;
plist->headnode->front = newnode;
plist->headnode = newnode;
}
if (newnode->ndata.prior < pmove->ndata.prior) {//在中間的的位置找到了,就是雙向連結串列的合適位置插入,可以參照我之前的部落格
newnode->front = pmove->front;
pmove->front->next = newnode;
newnode->next = pmove;
pmove->front = newnode;
break;
}
pmove = pmove->next;//一定要移動,要不然插入的是個屁,我就這裡沒表示,然後一直給我拋異常,我就好久才發現
}
if (pmove == plist->tailnode) {//當沒找到合適位置,意味著,優先順序最小,就是尾結點插入法
newnode->front = plist->tailnode;
plist->tailnode->next = newnode;
newnode->next = NULL;
plist->tailnode = newnode;
}
}
plist->size++;
}
接下來就是出隊操作,就是平常的鏈式隊列出隊,也就是頭節點刪除法:
void pop(struct listpriqueue* plist, struct data* pdata) {
if (isempty(plist)) {
printf("隊為空,無法出棧");
return;
}
*pdata = plist->headnode->ndata;
struct node* deletenode = plist->headnode;
if (plist->size == 1) {
free(deletenode);
plist->headnode = NULL;
}
else {
plist->headnode = plist->headnode->next;
free(deletenode);
}
plist->size--;
}
啊,這時候,螢幕面前一定有長得非常帥的小夥子問了哈,為什麼你陣列那裡要出隊的時候實現優先順序最大出列,到這就成了入隊的時候優先順序在前了呢。
我來分析一波,插入的時候是不是找到合適位置就可以停下來了,而出隊的時候需要掃描完才能確定,所以,插入實現優先最好情況是O(1)最壞情況是O(n),但是出隊實現優先的是O(n),不管是不是最壞還是最好(排好的情況),所以,某種意義上是插入的時候實現最好。
下面是主函式來實現上面的問題:
int main() {
struct listpriqueue* pqueue = creatlistpriqueue();
struct data readdata;
FILE* fp = fopen("test.txt", "r");
if (fp == NULL) {
printf("無法開啟資料夾");
return 0;
}
while (fscanf(fp, "%d %d\n", &readdata.num, &readdata.prior) != EOF){
push(pqueue, readdata);
}
fclose(fp);
int workindex = 1;
while (!isempty(pqueue)) {
pop(pqueue, &readdata);
printf("\t%d\t%d\t%d\n", workindex, readdata.num, readdata.prior);
workindex++;
}
return 0;
}
好了線性的優先佇列就到這了。