1. 程式人生 > >佇列(Queue):“先進先出”的資料結構

佇列(Queue):“先進先出”的資料結構

佇列是線性表的一種,在操作資料元素時,和棧一樣,有自己的規則:使用佇列存取資料元素時,資料元素只能從表的一端進入佇列,另一端出佇列,如圖1。 圖1 佇列示意圖
稱進入佇列的一端為“隊尾”;出佇列的一端為“隊頭”。資料元素全部由隊尾陸續進佇列,由隊頭陸續出佇列。

佇列的先進先出原則

佇列從一端存入資料,另一端調取資料的原則稱為“先進先出”原則。(first in first out,簡稱“FIFO”)

圖1中,根據佇列的先進先出原則,(a1,a2,a3,…,an)中,由於 a1 最先從隊尾進入佇列,所以可以最先從隊頭出佇列,對於 a2 來說,只有 a1 出隊之後,a2 才能出隊。

類似於日常生活中排隊買票,先排隊(入佇列),等自己前面的人逐個買完票,逐個出佇列之後,才輪到你買票。買完之後,你也出佇列。先進入佇列的人先買票並先出佇列(不存在插隊)。

佇列的實現方式

佇列的實現同樣有兩種方式:順序儲存鏈式儲存
兩者的區別同樣在於資料元素在物理儲存結構上的不同。

佇列的順序表示和實現

使用順序儲存結構表示佇列時,首先申請足夠大的記憶體空間建立一個數組,除此之外,為了滿足佇列從隊尾存入資料元素,從隊頭刪除資料元素,還需要定義兩個指標分別作為頭指標和尾指標。

當有資料元素進入佇列時,將資料元素存放到隊尾指標指向的位置,然後隊尾指標增加 1;當刪除對頭元素(即使想刪除的是佇列中的元素,也必須從隊頭開始一個個的刪除)時,只需要移動頭指標的位置就可以了。

順序表示是在陣列中操作資料元素,由於陣列本身有下標,所以佇列的頭指標和尾指標可以用陣列下標來代替,既實現了目的,又簡化了程式。

例如,將佇列(1,2,3,4)依次入隊,然後依次出隊並輸出。

程式碼實現:
#include <stdio.h>
int enQueue(int *a,int rear,int data){
    a[rear]=data;
    rear++;
    return rear;
}
void deQueue(int *a,int front,int rear){
    //如果 front==rear,表示佇列為空
    while (front!=rear) {
        printf("%d",a[front]);
        front++;
    }
}
int main() {
    int a[100];
    int front,rear;
    //設定隊頭指標和隊尾指標,當佇列中沒有元素時,隊頭和隊尾指向同一塊地址
    front=rear=0;
    //入隊
    rear=enQueue(a, rear, 1);
    rear=enQueue(a, rear, 2);
    rear=enQueue(a, rear, 3);
    rear=enQueue(a, rear, 4);
    //出隊
    deQueue(a, front, rear);
    return 0;
}

順序儲存存在的問題

當使用線性表的順序表示實現佇列時,由於按照先進先出的原則,佇列的隊尾一直不斷的新增資料元素,隊頭不斷的刪除資料元素。由於陣列申請的空間有限,到某一時間點,就會出現 rear 佇列尾指標到了陣列的最後一個儲存位置,如果繼續儲存,由於 rear 指標無法後移,就會出錯。
在陣列中做刪除資料元素的操作,只是移動了隊頭指標而沒有釋放所佔空間。 陣列真的滿了嗎?隊頭由於刪除元素,front 後移, front 前邊還會有可以使用的空間。所以為了充分利用這部分空間,可以考慮使用下面這種方式。

順序儲存的升級版

使用陣列存取資料元素時,可以將陣列申請的空間想象成首尾連線的環狀空間使用
。例如,在申請的記憶體空間大小為 5 的情況下,將數字 1-6 進隊後再出隊(普通方式中 6 是無法進隊的):

程式碼實現:
#include <stdio.h>
#define max 5
int enQueue(int *a,int front,int rear,int data){
    //迴圈佇列中,如果尾指標和頭指標重合,證明陣列存放的資料已滿
    if ((rear+1)%max==front) {
        printf("空間已滿");
        return rear;
    }
    a[rear%max]=data;
    rear++;
    return rear;
}
int  deQueue(int *a,int front,int rear){
    //如果front==rear,表示佇列為空
    if(front==rear) {
        printf("佇列為空");
        return front;
    }
    printf("%d",a[front]);
    front=(front+1)%max;
    return front;
}
int main() {
    int a[max];
    int front,rear;
    //設定隊頭指標和隊尾指標,當佇列中沒有元素時,隊頭和隊尾指向同一塊地址
    front=rear=0;
    //入隊
    rear=enQueue(a,front,rear, 1);
    rear=enQueue(a,front,rear, 2);
    rear=enQueue(a,front,rear, 3);
    rear=enQueue(a,front,rear, 4);
    //出隊
    front=deQueue(a, front, rear);
   
    rear=enQueue(a,front,rear, 5);
   
    front=deQueue(a, front, rear);
    rear=enQueue(a,front,rear, 6);
    front=deQueue(a, front, rear);
    front=deQueue(a, front, rear);
    front=deQueue(a, front, rear);
    front=deQueue(a, front, rear);
    return 0;
}
執行結果:
123456

在使用迴圈佇列判斷陣列是否已滿時,出現下面情況:
  • 當佇列為空時,佇列的頭指標等於佇列的尾指標
  • 當陣列滿員時,佇列的頭指標等於佇列的尾指標

要將空佇列和佇列滿的情況區分開,辦法是:犧牲掉陣列中的一個儲存空間,判斷陣列滿員的條件是:尾指標的下一個位置和頭指標相遇,就說明陣列滿了。

佇列的鏈式表示和實現(簡稱為“鏈佇列”)

佇列的鏈式儲存是在連結串列的基礎上,按照“先進先出”的原則操作資料元素。

例如,將佇列(1,2,3,4)依次入隊,然後再依次出隊。

程式碼實現:
#include <stdio.h>
#include <stdlib.h>
typedef struct QNode{
    int data;
    struct QNode * next;
}QNode;
QNode * initQueue(){
    QNode * queue=(QNode*)malloc(sizeof(QNode));
    queue->next=NULL;
    return queue;
}
QNode* enQueue(QNode * rear,int data){
    QNode * enElem=(QNode*)malloc(sizeof(QNode));
    enElem->data=data;
    enElem->next=NULL;
    //使用尾插法向鏈佇列中新增資料元素
    rear->next=enElem;
    rear=enElem;
    return rear;
}
void DeQueue(QNode * front,QNode * rear){
    if (front->next==NULL) {
        printf("佇列為空");
        return ;
    }
    QNode * p=front->next;
    printf("%d",p->data);
    front->next=p->next;
    if (rear==p) {
        rear=front;
    }
    free(p);
}
int main() {
    QNode * queue,*front,*rear;
    queue=front=rear=initQueue();//建立頭結點
    //向鏈佇列中新增結點,使用尾插法新增的同時,隊尾指標需要指向連結串列的最後一個元素
    rear=enQueue(rear, 1);
    rear=enQueue(rear, 2);
    rear=enQueue(rear, 3);
    rear=enQueue(rear, 4);
    //入隊完成,所有資料元素開始出佇列
    DeQueue(front, rear);
    DeQueue(front, rear);
    DeQueue(front, rear);
    DeQueue(front, rear);
    DeQueue(front, rear);
    return 0;
}
執行結果:
1234佇列為空

使用鏈佇列的心得體會

在使用鏈佇列時,最簡便的方法就是連結串列的表頭一端表示佇列的隊頭,表的另一端表示佇列的隊尾,這樣的設定會使程式更簡單。 反過來的話,佇列在增加元素的時候,要採用頭插法,在刪除資料元素的時候,由於要先進先出,需要刪除連結串列最末端的結點,就需要將倒數第二個結點的next指向NULL,這個過程是需要遍歷連結串列的。
另外需要注意的是,在刪除佇列中資料元素的時候,每次都需要判斷佇列是否為空,這就需要尋找一個判斷佇列為空的條件:如果頭結點的指標域為NULL,說明佇列為空;如果隊頭和隊尾指標都指向頭結點,說明佇列為空。(二選一)

使用鏈佇列解決問題時,要避免“野指標”的出現:

  1. 當刪除最後一個數據元素時,由於一貫地認為資料元素出佇列只跟隊頭指標有關係,會忽略隊尾指標。
  2. 當鏈佇列中只剩有一個數據元素時,隊尾指標指向的就是這個資料元素,被刪除後,隊尾指標指向的記憶體空間被釋放,還有可能給別的程式使用。這時候,隊尾指標如果不進行重定義,就會變成“野指標”。