Hello,everybody.我們又見面了。今天我們來學習一下隊列這個數據結構,let’s Go,開始我們的征程吧。
首先,舉兩個生活中的常見例子。相信大家,在用電腦工作娛樂時,都會碰到這樣的現象。當我們點擊程序或進行其他操作時,電腦處於死機狀態。正當我們准備Reset時,它突然像打了雞血似的,突然把剛才我們的操作,按順序執行了一遍。之所以會出現這個現象,是因為操作系統的多個程序,需要通過一個管道輸出,而按先后順序排隊造成的。
還有有個例子,在我們打客服熱線時,有時會出現等待的現象。當其他客戶掛斷電話,客服人員才會接通我們的電話。因為客服人員相對於客戶而言,總是不夠的,當客戶量大於客服人員時,就會造成排隊等待的想象。
操作系統、客服系統,都是應用了一種數據結構才實現了這種先進先出的排隊功能,這個數據結構就是隊列。
隊列(Queue):是只允許在一端進行插入操作,在另一端進行刪除操作的線性表。
隊列也是一種特殊的線性表,是一種先進先出的線性表。允許插入的一端稱為表尾,允許刪除的一端稱為表頭。
上圖,很形象的表示了隊列的結構。排在前面的先出,排在后面的后出。換句話,先進的先出,后進額后出。我們在隊尾插入數據,隊頭刪除數據。
隊列的抽象數據類型:
同樣是線性表,隊列也有類似線性表的操作,不同的是,插入操作只能在隊尾,刪除操作只能在隊頭。
上圖是隊列的抽象數據類型。
順序存儲的隊列:
我們在學習線性表時,知道線性表分為順序存儲與鏈式存儲兩種存儲方式。隊列是特殊的線性表,所以它也分為兩種存儲方式。我們先來看看它的順序存儲結構吧。
隊列順序存儲的不足:
假設有n個元素,我們需要初始化一個長度大於n的數組,來存放這n個元素,下標為0的位置為隊頭。隊列的插入(入隊)操作,是在隊尾進行操作的,隊列中的其他元素不用移動。入隊操作的時間復雜度為O【1】.但是如果,是刪除(出隊)操作,需要在隊頭操作,需要移動隊列中所有元素,以確保我們隊頭(下標為0的位置)不為空。所以,時間復雜度為O【n】。
我們可以改進一下這個隊列,以提高它的效率。我們大可不必,把元素放在數組的前n個位置。也就是說,我們沒必要把下標為0的位置定位隊頭位置。如下圖:
為了避免當只有一個元素時,隊頭與隊尾重合,影響我們的操作。所以,我們引入了front、rear指針。front指向第一個元素的位置,rear指向最后一個元素的下一個位置。
這樣,當rear=front時,不是隊列中只有一個元素,而是隊列為空。
這樣我們在進行出隊操作時,隊列中其他元素就不用動了。我們的時間復雜度為o[1].
但是,我們的問題又來了,看下圖:
此時,rear已經超出了數組的界限,但是下標為0、1的位置還是空的,這樣是不是挺浪費的?此時,我們的循環隊列就橫空出世了。
循環隊列:隊列的頭尾相接的順序存儲結構稱為循環隊列.
如下圖:
這里有一個問題,大家看下圖:
此圖中,rear=front,此時隊滿。可是,我們剛才說rear=front時,隊列為空。那么,rear=front時,是空還是滿呢?對於這個問題,我們提供了2中解決方法。
方法一:初始化一個flag變量,當flag=1,rear==front時,隊列滿。當flag=0,rear==front時,隊列空。
方法二:當rear==front時,隊列為空。當rear與front中間僅差一個存儲單元時,隊列為滿。
這里,我們討論一下方法二。看下圖:
front與rear之間相處一個存儲單元,此時我們就說隊列已滿。因為rear有時>front,有時<front。我們假設隊列的 最大尺寸為QueueSize,那么我們可以得到計算隊列為滿的公式。
(rear+1)%QueueSize==front.
當rear>front時,rear-front就是隊列的長度。如下圖:
當rear<front時,此時的隊列長度分兩部分,一部分為QueueSize-front,另一部分為rear+0。如下圖:
將兩部分加在一起,就是隊列的長度。最后,我們得出計算隊列長度的通用公式:
(rear-front+QueueSize)%QueueSize
我們看一下循環隊列的順序存儲結構代碼:
typedef int QElemType
typedef struct
{
QElemType data[MAXSIZE];
int front;
int rear;
}SqQueue;
循環隊列的初始化代碼:
Status InitQueue(SqQueue *Q)
{
Q->front=0;
Q->rear=0;
return ok;
}
循環隊列求隊列長度:
int QueueLength(SqQueue Q)
{
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}
循環隊列的入隊操作
Status EnQueue(SqQueue *Q,QElemType e)
{
if((Q->rear+1)==Q->front)/*隊列滿的判斷*/
return ERROR;
Q->data[Q->rear]=e;
Q-rear=(Q->rear+1)%MAXSIZE
return Ok;
}
循環隊列的出隊操作
Status DeQueue(SqQueue *Q,QElemType *e)
{
if(Q->front=Q->rear)/*隊列空的判斷*/
return ERROR;
*e=Q->data[Q->front];
Q->front=(Q->front+1)%MAXSIZE;
return ok;
}
從這一段講解,我們發現,單單使用隊列的順序存儲結構,性能是不高的。我們應該使用循環隊列,但是循環隊列又面臨着數組溢出的問題,所以我們還有學習一下不用隊列長度的鏈式存儲結構。
隊列的鏈式存儲:
隊列的鏈式存儲,其實就是線性表的單鏈表。只不過,只能尾進頭出。我們把它簡稱為鏈隊列。為了操作的方便,我們把front指向頭結點,把rear指向終端結點。空隊時,front、rear都指向頭結點。如下圖: 

鏈隊列的結構:
typedef int QElemType;
/*結點的結構*/
typedef struct QNode
{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
/*鏈表的結構*/
typedef struct
{
QueuePtr front, rear;
}LinkQueue;
鏈隊的入隊操作:
Status EnQueue(LinkQueue *Q,QElemType e)
{
QueuePtr s=(QueuePtr)malloc(size(QNode));
if(!s)/*存儲分配失敗*/
exit(OVERFLOW);
s->data=e;
s->next=NULL;
Q->rear->next=s;
Q->rear=s;
return ok;
}
鏈隊的出對操作:
Status DeQueue(LinkQueue *Q,QElemType *e)
{
QueuePtr P;
if(Q-front==Q->rear)
return ERROR;
P=Q->front-next;
*e=p->data;
Q->front->next=p->next;
if(Q->rear==p)
Q->rear=Q->front;
free(p);
return ok;
}
我們來比較一下循環隊列與鏈隊的區別:
關於他們的區別,我們從兩方面來分析。時間、空間。
時間:時間復雜度都為O【1】,但是鏈隊在申請、釋放結點時會消耗一些時間。
空間:循環隊列需要固定的長度,會出現存儲元素數量,空間浪費的問題。鏈隊不會出現空間浪費的問題。
總的來說,當我們可以確定長度時,我們選擇循環隊列,否則使用鏈隊。
總結:
這一章,我們主要學習的數據結構是棧(stack)、隊列(Queue).
Stack:只允許在一端進行插入刪除操作。
Queue:只能在一端插入,另一端刪除。
Stack、Queue都是特殊的線性表。所以它們都可以用順序存儲結構來實現,但是都存在一些弊端。它們各自都有解決這些弊端的方法。
Stack,它把相同的數據類型的棧,存放在一個數組中,讓數組一頭為一個棧的棧頂,另一頭為另一個棧的棧頂。最大化的利用了數組空間。
Queue:為了避免出隊,而移動隊元素,於是引入了循環隊列,讓頭尾相連。使得時間復雜度為O【1】.
他們又都可以用鏈式存儲結構實現。
這就是這一章的內容了,接下來我們一起學習串。










