隊列的基本概念
隊列的定義
隊列(Queue):隊列簡稱隊,也是一種操作受限的線性表,只允許在表的一端進行插入,而在表的另一端進行刪除。向隊列中插入元素稱爲入隊或進隊;刪除元素稱爲出隊或離隊。這和我們日常生活中的排隊是一致的,最早排隊的也是最早離隊的。其操作的特性是先進先出(First In First Out, FIFO),故又稱爲先進先出的線性表。
隊頭(Front):允許刪除的一端,又稱爲首隊
隊尾(Rear):允許插入的一端
空隊列:不含任何元素的空表
隊列常見的基本操作
InitQueue(&Q):初始化隊列,構造一個空隊列
QueueEmpty(Q):判斷隊列空,若隊列Q爲空返回true,否則返回false
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=MaxSize-1後,再前進一個位置就可以自動到0,這可以利用除法取餘運算(%)來實現
初始時: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;
}