佇列相關習題及詳解(選擇題和綜合題) ——資料結構
佇列的基本概念
佇列的定義
佇列(Queue):佇列簡稱隊,也是一種操作受限的線性表,只允許在表的一端進行插入,而在表的另一端進行刪除。向佇列中插入元素稱為入隊或進隊;刪除元素稱為出隊或離隊。這和我們日常生活中的排隊是一致的,最早排隊的也是最早離隊的。其操作的特性是先進先出(First In First Out, FIFO),故又稱為先進先出的線性表。
隊頭(Front):允許刪除的一端,又稱為首隊
隊尾(Rear):允許插入的一端
空佇列:不含任何元素的空表
佇列常見的基本操作
InitQueue(&Q):初始化佇列,構造一個空佇列
QueueEmpty(Q):
EnQueue(&Q,x):入隊,若佇列Q未滿,將x加入,使之成為新的隊尾
DeQueue(&Q,&x):出隊,若佇列非空,刪除隊頭元素,並用x返回
GetHead(Q,&x):讀隊頭元素,若佇列Q非空,則將隊頭元素賦值給x
需要注意的是,佇列是操作受限的線性表,所以,不是任何對線性表的操作都可以作為佇列的操作。比如,不可以隨便讀取佇列中間的某個資料。
佇列的順序儲存結構
佇列的順序儲存
佇列的順序實現是指分配一塊連續的儲存單元存放佇列中的元素,並附設兩個指標front rear分別指示隊頭元素和隊尾元素的位置。設隊頭指標指向隊頭元素,隊尾指標指向隊尾元素的下一個位置
佇列的順序儲存型別可描述為:
#define MaxSize 50
typedef struct{
ElemType data[MaxSize];
int front,rear;
} SqQueue;
在佇列的初始狀態時,有Q.front==Q.rear==0成立,該條件可以作為判斷空的條件。但能否用Q.rear==MaxSize作為佇列滿的條件呢?顯然不能,因為有些情況佇列中只有一個元素,但仍滿足該條件。這時入隊出現“上溢位”,但這種溢位並不是真正的溢位,在data陣列中依然存在可以存放元素的空位置,所以是一種“假溢位”。
迴圈佇列
前面已經指出了順序佇列的缺點,這裡我們引出迴圈佇列的概念。將順序佇列臆造為一個環狀的空間,即把儲存佇列元素的表從邏輯上看成一個環,稱為迴圈佇列
初始時:Q.front=Q.rear=0;
隊首指標進1:Q.front=(Q.front+1)%MaxSize
隊尾指標進1:Q.rear=(Q.rear+1)%MaxSize
佇列長度:(Q.rear+MaxSize-Q.front)%MaxSize
為了區分隊空還是隊滿的情況,有三種處理方式
1)犧牲一個單元來區分隊空和堆滿,入隊時少用一個佇列單元,這是一種較為普遍的做法,約定以“隊頭指標在隊尾指標的下一個位置作為隊滿的標識”
隊滿條件為:(Q.rear+1)%MaxSize==Q.front
隊空條件為:Q.front==Q.rear
佇列中元素的個數:(Q.rear=Q.front+MaxSize)%MaxSize
2)型別中增設表示元素個數的資料成員。這樣,則隊空的條件為Q.size==0;隊滿的條件為Q.size=MaxSize.這兩種情況都有Q.front==Q.rear
3)型別中增設tag資料成員,以區分堆滿還是隊空。tag等於0的情況下,若因刪除導致Q.front=Q.rear則隊為空;tag等於1的情況下,若因插入導致Q.front=Q.rear則為隊滿。
迴圈佇列的操作
1)初始化
void InitQueue(&Q){
Q.rear=Q.front=0; //初始化隊首、隊尾指標
}
2)判隊空
bool isEmpyt(Q){
if(Q.rear==Q.front) return true; //隊空條件
else return false;
}
3)入隊
bool EnQueue(SqQueue &Q,ElemType x){
if((Q.rear+1)%MaxSize==Q.front) return false; //隊滿
Q.data[Q.rear]=x;
Q.rear=(Q.rear+1)%MaxSize; //隊尾指標加1取模
return true;
}
4)出隊
bool DeQueue(SqQueue &Q,ElemType &x){
if(Q.rear==Q.front) return false; //隊空,報錯
x=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize; //隊頭指標加1取模
return true;
}
佇列的鏈式儲存結構
佇列的鏈式儲存
佇列的鏈式表示稱為鏈佇列,它實際上是一個同事帶有隊頭指標和隊尾指標的單鏈表。頭指標向隊頭結點,尾指標指向尾結點,即單鏈表的最後一個結點(注意與順序儲存的不同)。佇列的鏈式儲存如圖所示
棧的鏈式儲存型別可以描述為
typedef struct //鏈式佇列結點
{
ElemType data;
struct LinkNode *next;
}LinkNode;
typedef struct //鏈式佇列
{
LinkNode *front,*rear; //佇列的隊頭和隊尾指標
}LinkQueue;
當Q.front==NULL且Q.rear=NULL時,鏈式佇列為空
出隊時,首先判斷隊是否為空,若不空,則去除隊頭元素,將其從連結串列中摘除,並讓Q.front指向下一個結點(若該結點為最後一個結點,則置Q.front和Q.rear都為NULL)入隊時,建一個新結點,將新結點插入到連結串列的尾部,並改讓Q.rear指向這個心插入的結點(若原佇列為空隊,則令Q.front也指向該結點)
不難看出,不設頭結點的鏈式佇列在操作上往往比較麻煩,因此,通常將鏈式佇列設計成一個帶頭結點的單鏈表,這樣插入和刪除操作就統一了。
用單鏈表表示的鏈式佇列特別適合於資料元素變動比較大的情形,而且不存在佇列滿且產生溢位的問題。另外加入程式中要使用多個佇列,與多個棧的情形一樣,最好使用鏈式佇列,這樣就不會出現儲存分配不合理和“溢位”的問題。
鏈式佇列的基本操作
1)初始化
void InitQueue(LinkQueue &Q){
Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode));//建立頭結點
Q.front->next=NULL; //初始為空
}
2)判隊空
bool IsEmpty(LinkQueue Q){
if(Q.front==Q.rear) return true;
else return false;
}
3)入隊
void EnQueue(LinkQueue &Q,ElemType x){
s=(LinkNode *)malloc(sizeof(LinkNode));
s->data=x;s->next=NULL; //建立新結點,插入到鏈尾
Q.rear->next=s;
Q.rear=s;
}
4)出隊
boole DeQueue(LinkQueue &Q,ElemType &x){
if(Q.front==Q.rear) return false;//空隊
p=Q.front->next;
x=p->data;
Q.front->next=p->next;
if(Q.rear==p)
Q.rear=Q.front; //若原佇列中只有一個結點
free(p);
}
習題部分
選擇題
第一題
棧和佇列的主要區別在於()
A. 它們的邏輯結構不一樣 B. 它們的儲存結構不一樣 C. 所包含的元素不一樣 D. 插入、刪除操作的限定不一樣
第二題
迴圈佇列儲存在陣列A[0…n],則入隊時的操作為()
A. rear=rear+1 B. rear=(rear+1)mod(n-1) C. rear=(rear+1)modn D. rear=(rear+1)mod(n+1)
第三題
若用陣列A[0..5]來實現迴圈佇列,且當前rear和front的值分別為1和5,當從佇列中刪除一個元素,再加入兩個元素後,rear和front的值分別為()
A. 3和4 B. 3和0 C. 5和0 D. 5和1
第四題
已知迴圈佇列儲存在一維陣列A[0…n-1],且佇列非空時,front和rear分別指向隊頭元素和隊尾元素。若初始時佇列為空,且要求第一個進入佇列的元素儲存在A[0]處,則初始時front和rear的值分別是()
A. 0,0 B. 0,n-1 C. n-1,0 D. n-1,n-1
第五題
迴圈佇列放在以為陣列A[0…M-1]中,end1指向隊頭元素,end2指向隊尾元素的後一個位置。假設佇列兩段均可進行入隊和出隊操作,佇列中最多能容納M-1個元素。初始時為空。下列判斷隊空和隊滿的條件中,正確的是()
A. 6 B. 4 C. 3 D. 2
第六題
最適合用作鏈隊的連結串列是()
A. 隊空:end1==en2; 隊滿:end1==(edn2+1)mod M
B. 隊空:end1==en2; 隊滿:end1==(edn1+1)mod (M-1)
C. 隊空:end2==(edn1+1)mod M 隊滿:end1==(edn2+1)mod M
D. 隊空:end1==(edn2+1)mod M 隊滿:end1==(edn1+1)mod (M-1)
第七題
最不適合用作鏈式佇列的連結串列是()
A. 只帶隊首指標的非迴圈雙鏈表 B. 只帶隊首指標的迴圈雙鏈表
C. 只帶隊尾指標的迴圈雙鏈表 D. 只帶隊尾指標的迴圈單鏈表
解答部分
第一題
棧和佇列的邏輯結構都是線性表,它們的儲存結構可能是順序的也可能是鏈式的,但不能說是它們的主要區別,C的道理也是一樣,只有D才是棧和佇列的本質區別。不管是順序儲存還是鏈式儲存,棧和佇列都只嗯呢該順序存取,而向量陣列是直接(隨機)存取
第二題
這道題需要注意的是,由於陣列的下標範圍是0-n,所以陣列的容量為n+1
迴圈佇列中新元素入隊的操作是rear=(rear+1)MOD maxsize,本題中maxsize=n+1。因此入隊操作應為rear=(rear+1)MOD(n+1)
第三題
迴圈佇列中,每刪除一個元素,隊首指標:front=(front+1)%6,每插入一個元素,隊尾指標:rear=(rear+1)%6。上述操作後,front=0,rear=3
第四題
根據題意,第一個元素進入佇列後儲存在A[0]處,此時front和rear值都為0。入隊時由於要執行(rear+1)%n ,所以如果入隊後指標都指向0,則rear初值為n-1,而由於第一個元素在A[0]中,插入操作只改變rear指標,所以front為0不變。
第五題
end1指向隊頭元素,可知出隊的操作是先從A[edn1]讀數,然後end1再+1。end2指向對尾元素的後一個位置,可知入隊操作是先存數到A[end2],然後end2再加1。若把A[0]儲存第一個元素,當佇列初始時,入隊操作是把資料放到A[0],然後end2自增,即可知end2初值為0;
而end1指向的是隊頭元素,隊頭元素的在陣列A中的下標為0,所以得知end1初值也為0,可知隊空條件為end1==end2;
然後考慮佇列滿時,因為佇列最多能容納M-1個元素,假設佇列儲存在下標為0到下標為M-2的M-1個區域,隊頭為A[0],隊尾為A[M-2],此時佇列滿,考慮在這種情況下end1和end2的狀態,end1指向隊頭元素,可知end1=0,end2指向對尾元素的後一個位置,可知end2=M-2+1=M-1,所以可知隊滿的條件為end1==(end2+1)mod M, 選A
最好還是畫圖看著題
第六題
由於對了需在雙端進行操作,選項C和D的連結串列顯然不太適合鏈隊。選項A的連結串列在完成進隊和出隊後還要修改為迴圈的,對於佇列來講是多餘的。對於選項B,由於有首指標,適合刪除首結點;由於有尾指標,適合在其後面插入結點,故選B
第七題
由於非迴圈雙鏈表只帶隊首指標,可在執行入隊操作時需要修改隊尾結點的指標域,而查詢隊尾結點需要O(n)的時間。BCD均可在O(1)的時間內找到隊首和隊尾。
綜合題
習題部分
第一題
如果希望迴圈佇列中的元素都能得到利用,則需設定一個標誌域tag,並以tag的值為0或1來區分頭指標front和隊尾指標rear相同時的佇列狀態是“空”還是“滿”,試編寫與此結構相應的入隊和出隊演算法
第二題
Q是一個佇列,S是一個空棧,實現將佇列中的元素逆置的演算法
第三題
利用兩個棧S1/ S2模擬一個佇列,已知棧的4個運算定義如下:
Push(S,x); //元素x入棧S
Pop(S,x); //S出棧並將出棧的值賦給x
StackEmpty(S); //判斷棧是否為空
StackOverflow(S); //判斷棧是否滿
那麼如何利用棧的運算來實現該佇列的3個運算(形參由做題這根據要求自己設計)
Enqueue; //將元素x入隊
Dequeue; //出隊,並將出隊元素儲存在x中
QueueEmpty; //判斷佇列是否為空
解答部分
第一題
在迴圈佇列的型別結構中,增設一個tag的整形變數,進隊時置tag為1,出對時置tag為0(因為只有入隊操作可能導致隊滿,也只有出對操作可能導致隊空)。佇列Q初始時,tag=0、front=rear=0。這樣的佇列4要素如下:
隊空條件:Q.front=Q.rear 且Q.tag==0
隊滿條件:Q.front==Q.rear且Q.tag==1
進隊操作:Q.data[Q.rear]=x;Q.rear=(Q.rear+1)/MaxSize;Q.tag=1
出隊操作:x.Q.data[Q.front];Q.front=(Q.front+1)/MaxSize;Q.tag=0
1)設“tag”法的迴圈佇列入隊演算法:
int EnQueue1(SqQueue &Q ,ElemType x){
if(Q.front==Q.rear&&Q.tag==1)
return 0; //連個條件都滿足時則隊滿
Q.data[Q.rear]=x;
Q.rear=(Q.rear+1)%MaxSize;
Q.tag=1; //可能隊滿
return 1;
}
2)設“tag”法的迴圈佇列入隊演算法:
int DeQueue1(SqQueue &Q,ElemType &x){
if(Q.front==Q.rear&&Q.tag==0)
return 0; //兩個條件都滿足時則隊空
x=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize;
Q.tag=0; //可能隊空
return 1;
}
第二題
主要考察隊佇列和棧的特性和操作的理解。只是對佇列的一系列操作是不可能將其中的元素逆置的,而棧可以將入棧的元素逆序提取出來。所以,我們可以將佇列中的元素逐個地出佇列,入棧;全部入棧後再逐個出棧,如佇列。
void Inverser(Stack S,Queue Q){
//本演算法實現將佇列中的元素逆置
while(!=QueueEmpty(Q)){
x=DeQueue(Q); //佇列中全部元素一次出隊
Push(S,x); //元素一次入棧
}
while(!StackEmpty(S)){
Pop(S,x); //棧中全部元素一次出棧
EnQueue(Q,x); //再入隊
}
}
第三題
利用兩個棧S1和S2來模擬一個佇列,當需要向佇列中插入一個元素時,用S1來存放已輸入的元素,即S1執行入棧操作。當需要出隊時,則隊S2執行出棧操作。由於從棧中去除元素的順序是原順序的逆序,所以,必須先將S1中的所有元素全部出棧併入棧到S2中,再在S2中執行出棧操作,即可實現出隊操作,而在執行此操作前必須判斷S2是否為空,否則會導致順序混亂。當棧S1和S2都為空時,佇列為空。
總結如下:
1)對S2的出棧操作用作出隊,若S2為空,則先將S1中的鄋元素送入S2
2)對S1的入棧操作用作入隊,若S1滿,必須先保證S2為空,才能將S1中的元素全部插入S2中。
入隊演算法
int EnQueue(Stack S1,Stack S2,ElemType e){
if(!StackOverflow(S1)){
Push(S1,x);
return 1;
}
if(StackOverflow(S1)&&!StackEmpty(S2)){
printf("佇列滿\n");
return 0;
}
if(StackOverflow(S1)&&StackEmpty(S2)){
while(!=StackEmpty(S1)){
Pop(S1,x);
Push(S2,x);
}
}
Push(S1,x);
return 1
}
出隊演算法
void DeQueue(Stack S1,Stack S2,ElemType &x){
if(!StackEmpty(S2)){
Pop(S2,x);
}
else if (StackEmpty(S1)){
printf("佇列為空\n");
}
else{
while(!=StackEmpty(S1)){
Pop(S1,x);
Push(S2,x);
}
Pop(S2,x);
}
}
判斷佇列為空的演算法:
int QueueEmpty(Stack S1,Stack S2){
if(StackEmpty(S1)&&StackEmpty(S2))
return 1;
else
return 0;
}