佇列(Queue):“先進先出”的資料結構
阿新 • • 發佈:2018-12-23
佇列是線性表的一種,在操作資料元素時,和棧一樣,有自己的規則:使用佇列存取資料元素時,資料元素只能從表的一端進入佇列,另一端出佇列,如圖1。
圖1 佇列示意圖
稱進入佇列的一端為“隊尾”;出佇列的一端為“隊頭”。資料元素全部由隊尾陸續進佇列,由隊頭陸續出佇列。
圖1中,根據佇列的先進先出原則,(a1,a2,a3,…,an)中,由於 a1 最先從隊尾進入佇列,所以可以最先從隊頭出佇列,對於 a2 來說,只有 a1 出隊之後,a2 才能出隊。
兩者的區別同樣在於資料元素在物理儲存結構上的不同。
當有資料元素進入佇列時,將資料元素存放到隊尾指標指向的位置,然後隊尾指標增加 1;當刪除對頭元素(即使想刪除的是佇列中的元素,也必須從隊頭開始一個個的刪除)時,只需要移動頭指標的位置就可以了。
順序表示是在陣列中操作資料元素,由於陣列本身有下標,所以佇列的頭指標和尾指標可以用陣列下標來代替,既實現了目的,又簡化了程式。
例如,將佇列(1,2,3,4)依次入隊,然後依次出隊並輸出。
程式碼實現:
在陣列中做刪除資料元素的操作,只是移動了隊頭指標而沒有釋放所佔空間。 陣列真的滿了嗎?隊頭由於刪除元素,front 後移, front 前邊還會有可以使用的空間。所以為了充分利用這部分空間,可以考慮使用下面這種方式。 。例如,在申請的記憶體空間大小為 5 的情況下,將數字 1-6 進隊後再出隊(普通方式中 6 是無法進隊的):
程式碼實現:
123456
在使用迴圈佇列判斷陣列是否已滿時,出現下面情況:
例如,將佇列(1,2,3,4)依次入隊,然後再依次出隊。
程式碼實現:
1234佇列為空
另外需要注意的是,在刪除佇列中資料元素的時候,每次都需要判斷佇列是否為空,這就需要尋找一個判斷佇列為空的條件:如果頭結點的指標域為NULL,說明佇列為空;如果隊頭和隊尾指標都指向頭結點,說明佇列為空。(二選一)
使用鏈佇列解決問題時,要避免“野指標”的出現:
稱進入佇列的一端為“隊尾”;出佇列的一端為“隊頭”。資料元素全部由隊尾陸續進佇列,由隊頭陸續出佇列。
佇列的先進先出原則
佇列從一端存入資料,另一端調取資料的原則稱為“先進先出”原則。(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 前邊還會有可以使用的空間。所以為了充分利用這部分空間,可以考慮使用下面這種方式。
順序儲存的升級版
使用陣列存取資料元素時,可以將陣列申請的空間想象成首尾連線的環狀空間使用程式碼實現:
#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,說明佇列為空;如果隊頭和隊尾指標都指向頭結點,說明佇列為空。(二選一)
使用鏈佇列解決問題時,要避免“野指標”的出現:
- 當刪除最後一個數據元素時,由於一貫地認為資料元素出佇列只跟隊頭指標有關係,會忽略隊尾指標。
- 當鏈佇列中只剩有一個數據元素時,隊尾指標指向的就是這個資料元素,被刪除後,隊尾指標指向的記憶體空間被釋放,還有可能給別的程式使用。這時候,隊尾指標如果不進行重定義,就會變成“野指標”。