棧和佇列:2.佇列(Queue)及其C語言實現
佇列是線性表的一種,在操作資料元素時,和棧一樣,有自己的規則:使用佇列存取資料元素時,資料元素只能從表的一端進入佇列,另一端出佇列,如圖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,說明佇列為空;如果隊頭和隊尾指標都指向頭結點,說明佇列為空。(二選一)
使用鏈佇列解決問題時,要避免“野指標”的出現:
- 當刪除最後一個數據元素時,由於一貫地認為資料元素出佇列只跟隊頭指標有關係,會忽略隊尾指標。
- 當鏈佇列中只剩有一個數據元素時,隊尾指標指向的就是這個資料元素,被刪除後,隊尾指標指向的記憶體空間被釋放,還有可能給別的程式使用。這時候,隊尾指標如果不進行重定義,就會變成“野指標”。