十、【栈和队列】队列
隊列 Queue
隊列和棧一樣,也屬于受限線性表。
1 隊列的基本概念
1.1 隊列的定義
隊列是只允許在表尾進行插入,表頭進行刪除的線性表。插入操作又稱為入隊,刪除操作又稱為出隊。
隊列的邏輯關系就和現實和生活中的隊列一樣,最早排隊就最早離隊,這種特性被稱為“先入先出”(First In First Out, FIFO)。
1.2 隊列的基本操作
隊列的基本操作和棧類似,不同的是刪除操作在隊首執行。
| InitQueue(&Q) | 初始化一個空隊列 Q。 |
| DestroyQueue(&Q) | 銷毀隊列,并釋放隊列 Q 占用的存儲空間。 |
| ClearQueue(&Q) | 將 Q 清空為空隊列。 |
| QueueEmpty(Q) | 判斷隊列 Q 是否為空,若為空則返回 true,否則返回 false。 |
| QueueLength(Q) | 返回隊列 Q 的元素個數,即隊列的長度。 |
| GetHead(Q, &x) | 讀取隊首元素,若隊列非空,則用 x 返回隊首元素。 |
| EnQueue(&Q, x) | 入隊,若隊列 Q 未滿,則將 x 插入,使之成為新隊尾。 |
| DeQueue(&Q, &x) | 出隊,若隊列 Q 非空,則彈出隊首元素,并用 x 返回。 |
2 隊列的順序表示和實現
2.1 隊列的順序表示
由于隊列和棧很相似,所以自然地想到用類似順序棧的結構來實現隊列的順序表示。隊列的順序存儲通常以數組實現,并設置兩個指針:頭指針 front 指向隊首元素,尾指針 rear 指向隊尾元素的下一位。但是因為隊列的首尾指針都可以移動,所以隊列實現的重點是隊列為空和隊列為滿的判斷。
- 起始狀態隊列為空,front 和 rear 指向隊首:Q.front = Q.rear = 0 。
- 入隊時,先給 rear 指向的位置賦值,然后 rear 指針向后移動。
- 出隊時,先保存 front 指向位置的值,然后 front 指針向后移動。
- 隊列為空時,Q.front = Q.rear。
- 隊列為滿時,Q.rear-Q.front = MAXSIZE。
看似上述結構順利地實現了順序隊列,但是這種實現方式其實是有問題的。假設我們現在創建了一個容量為6的隊列,然后向隊尾插入6個數,再從隊首刪除5個數,會導致 rear 指向隊尾的后一位,front 指向隊尾元素(如下圖 (3))。此時隊列未滿,按理說可以繼續從表尾插入元素,但是 rear 已經超過了數組的范圍,所以無法繼續插入。如果選擇擴容,確實可以解決無法插入的問題,但是在隊列未滿的情況下是不應該發生擴容操作的,因此不符合邏輯。
一種可行的解決辦法是將 rear 和 front 指針向下平移5個元素,使 front 重新指向數組的第一個元素。但是這種方法顯然不夠好,因為隊列的主要操作就是刪除和插入操作,這就需要頻繁地進行平移操作來維護隊列的結構,會增加隊列操作的時間復雜度,所以我們引入另一種解決辦法——循環隊列。
2.2 循環隊列
循環隊列是通過取余運算將線性結構處理為“環形結構”的隊列。循環隊列并不是真的像循環鏈表那樣,使用指針將表的首尾元素相連,而是借用了取余運算把隊列從邏輯上視為一個環。
2.2.1 取余運算的作用
在循環鏈表中,我們將指針后移一位的操作定義為:i = (i+1) % MAXSIZE。它和直接后移操作 i = i+1 產生的區別如下:
| 0 | 1 | 1 |
| 1 | 2 | 2 |
| 2 | 3 | 3 |
| 3 | 4 | 4 |
| 4 | 5 | 5 |
| 5 | 6 | 0 |
可以看到,盡管實際的數組還是線性結構,取余運算卻可以從隊尾重新定位回隊首,在邏輯上將線性結構轉化為循環結構。這一特點在以后也會經常用到。
2.2.2 循環隊列的特點
循環隊列的示意圖如下:
一般循環隊列的 front 指向隊首元素,rear 指向隊尾元素的下一位置(循環隊列中需要預留一位)
- 起始狀態隊列為空,front 和 rear 指向隊首:Q.front = Q.rear = 0 。
- 入隊時,先給 rear 指向的位置賦值,然后 rear 指針向后移動:Q.rear = (Q.rear + 1) % MAXSIZE 。
- 出隊時,先保存 front 指向位置的值,然后 front 指針向后移動:Q.front = (Q.front + 1) % MAXSIZE 。
- 隊列長度為:(Q.rear + MAXSIZE - Q.front) % MAXSIZE。
- 隊列為空時:front = rear。
- 隊列為滿時:(rear+1) % MAXSIZE = front 。
在隊列為空時,依舊有 front = rear,但是隊列為滿的情形(如上圖 (4))變成了:(rear+1) % MAXSIZE = front 。如果 rear 是指向隊尾元素而不是隊尾元素的下一位,那么隊列為滿的情形就如上圖 (5),也有 front = rear。這個時候僅依靠兩個指針便無法判斷到底是滿隊列還是空隊列,除非使用額外的信息來幫助判斷,例如隊列的有效長度或其他標識。
2.3 循環隊列的實現
循環隊列可以用靜態數組或動態數組來實現,因為靜態循環隊列的實現比較簡單,因此不再贅述,詳情見附錄。這里主要介紹動態循環隊列的實現。
2.3.1 動態循環隊列類型定義
可以借鑒動態順序棧:
/********** 動態循環隊列的類型定義 **********/ #define INITSIZE 6 #define INCREMENT 6typedef int ElemType; typedef struct {ElemType *data;int front;int rear;int size; // 隊列的總容量(包含預留給rear指針的空位置) } SqQueue;2.3.2 主要操作的實現
隊列和棧一樣,涉及的主要操作有8個,這里主要介紹讀取隊首元素操作、出隊操作和入隊操作,其中入隊操作稍微復雜一些。
2.3.2.1 讀取隊首元素
/** Function: 讀取隊首元素操作* ----------------------------* 讀取隊首元素,若隊列非空,則用e返回隊首元素。*/ bool GetHead(SqQueue Q, ElemType &e){if (QueueEmpty(Q)){return false;}e = Q.data[Q.front];return true; }時間復雜度分析
因為隊列包含隊首和隊尾指針,因此只需要一步就可以獲取隊首元素,讀取隊首元素操作的時間復雜度為 O(1)O(1)O(1) 。
2.3.2.2 出隊操作
/** Function: 出隊操作* ----------------------------* 若隊列Q非空,則彈出隊首元素,并用e返回。*/ bool DeQueue(SqQueue &Q, ElemType &e){if (GetHead(Q, e)){Q.front = (Q.front+1) % Q.size;return true;}return false; }時間復雜度分析
可以借助讀取隊首元素操作,首先讀取隊首元素,然后將隊首指針向后移動一位,總的時間復雜度仍為 O(1)O(1)O(1) 。
2.3.2.3 入隊操作
擴容操作可能存在的問題
動態循環隊列的入隊操作支持在存儲空間不夠使時申請內存擴容,新的擴容空間會插入動態數組的尾部,實際數組的變化如下:
而對于我們想象中的循環隊列,擴容后變化如下。下圖(2)紅色的部分就是新加入的空間。可以看到,擴容操作成功地將隊列的存儲空間增加,但同時也修改了隊列原有元素的邏輯關系,可能會導致隊列的結構遭到破壞。例如,擴容前的循環隊列中,隊尾指針 rear 指向的元素(3)和隊首指針 front 指向的元素(0)是相鄰的。而在擴容后,新的空間被插入這兩個指針之間,導致它們指向的元素不再相鄰,破壞了這兩個元素之間的邏輯關系。
讓我們用另外一個例子來進一步說明,假設現在的隊列如下圖(1)所示,需要進行擴容操作。進行擴容后,得到下圖(2)所示的隊列。這個時候,元素 bbb 和 ccc 之間的邏輯關系被破壞。并且,盡管隊列現在有多余空間,但按照原有的判滿條件,該隊列依然是滿隊列,我們無法向隊列中插入任何值。因此,我們在擴容操作后還需要檢查隊列的原有元素關系是否遭到破壞,如被破壞需要主動維護。
維護擴容后的隊列結構
那么如何維護擴容后的隊列結構呢,最直觀的辦法就是將斷裂的后半部分平移到一個符合原來邏輯關系的新位置。例如,上圖中的完整隊列 front->a->b->c->rear 斷裂為 front->a->b 和 c->rear 兩半部分。我們可以把元素 ccc 從位置 0 移動到位置 4,把 rear 指針從位置 1 移動到位置 5。這樣,就又構成了一個完整的隊列。
整個過程需要兩個輔助指針 tmp1 和 tmp2,tmp1 指向后半部分隊列的第一個元素,tmp2 指向前半部分隊列的隊尾元素的下一位。而通過觀察可以發現,每一次擴容時插入操作必然發生在隊列的第一個位置(不等于隊首指針)和最后一個位置(不等于隊尾指針)之間。因此,tmp1 和 tmp2 的位置實際上也是固定的,
- tmp1 指向數組的第一個元素,tmp1 = 0 。
- tmp2 指向擴容前數組的最后一個元素的后一位,tmp2 = Q.size 。
入隊操作的實現
/** Function: 入隊操作* ----------------------------* 若隊列Q未滿,則將e插入,使之成為新隊尾,* 否則先擴容,再插入。*/ bool EnQueue(SqQueue &Q, ElemType e){int oldSize = Q.size;if (QueueLength(Q)>=Q.size-1){ // 需要擴容Q.data = (ElemType*) realloc (Q.data, (Q.size+INCREMENT) * sizeof(ElemType));Q.size += INCREMENT;printf("Increment\n");} // 擴容完成后隊列可能出現斷裂if (Q.front > Q.rear){int tmp1 = 0; // 兩個臨時指針分別指向兩個斷裂處int tmp2 = oldSize; while (tmp1!=Q.rear){Q.data[tmp2++] = Q.data[tmp1++];}Q.rear = tmp2;}// 執行插入Q.data[Q.rear] = e;Q.rear = (Q.rear+1) % Q.size;return true; }時間復雜度分析
- 最優情況:無需擴容,直接插入,時間復雜度為 O(1)O(1)O(1) 。
- 最差情況:先擴容,然后維護隊列結構,最后再執行插入操作。假設 realloc 函數重新分配空間并且需要復制元素,那么擴容操作需要時間 O(n)O(n)O(n) ;維護隊列結構最多需要移動 nnn 次,時間復雜度為 O(n)O(n)O(n) ;插入操作需要一步,時間復雜度為 O(1)O(1)O(1)。因此,總的時間復雜度為 O(n)O(n)O(n)。
3 隊列的鏈式表示和實現
3.1 隊列的鏈式表示
隊列的鏈式表示稱為鏈隊列,是一個同時帶有頭指針和尾指針的單鏈表,頭指針指向頭結點,尾指針指向隊尾結點。鏈隊列的大部分操作和單鏈表一致,只是插入操作只能在表尾進行,刪除操作只能在表頭進行。
- 在隊列為空時,只有頭結點,因此 Q.front = Q.rear。
- 入隊時,將新結點加入鏈表尾部,然后將尾指針后移。若原隊列為空,則頭指針也需移動。
- 出隊時,若隊列非空,則刪除第一個結點,頭指針后移。
- 求隊列長度需要遍歷鏈表,從頭指針出發,到尾指針結束。
- 隊列為空的判斷條件是兩個指針重合,Q.front = Q.rear。
鏈隊列適合數據元素變動較大的情形,且不存在隊列滿導致溢出的問題。
3.2 鏈隊列的實現
3.2.1 鏈隊列的類型定義
先定義結點類,然后定義鏈隊列。鏈隊列至少要包含兩個指針,還可以儲存如隊列的長度這樣的輔助信息。
/********** 鏈隊列的類型定義 **********/ typedef int ElemType;typedef struct LNode{ ElemType data; // 數據域struct LNode *next; // 指針域 } LNode;typedef struct {LNode *front, *rear; } LinkedQueue;3.2.2 主要操作的實現
鏈隊列大部分操作的時間復雜度和單鏈表一致,但是因為保留了尾指針,所以鏈隊列的尾部插入操作時間復雜度僅為 O(1)O(1)O(1) 。實現詳情見附錄。
4 雙端隊列
除了棧和隊列外,還有一種受限線性表叫雙端隊列(Double End Queue,也叫 Deque)。其特點是隊列的兩端都可以執行插入或刪除操作,兩端分別叫做端點 1 和端點 2。在實際使用中,還有輸出受限的雙端隊列和輸入受限的雙端隊列:
- 輸出受限的雙端隊列:兩端都可以輸入,但只有一端可以輸出。
- 輸入受限的雙端隊列:兩端都可以輸出,但只有一端可以輸入。
由于雙端隊列在實際應用中并不實用,所以僅列出了解。例題見此處
相關章節
第一節 【緒論】數據結構的基本概念
第二節 【緒論】算法和算法評價
第三節 【線性表】線性表概述
第四節 【線性表】線性表的順序表示和實現
第五節 【線性表】線性表的鏈式表示和實現
第六節 【線性表】雙向鏈表、循環鏈表和靜態鏈表
第七節 【棧和隊列】棧
第八節 【棧和隊列】棧的應用
第九節 【棧和隊列】棧和遞歸
第十節 【棧和隊列】隊列
附錄
隊列的順序實現
靜態循環隊列
/** Filename:StaticSqQueue.h* -----------------------* 靜態數組實現循環隊列。*/#ifndef _STATIC_SEQUENTIAL_QUEUE_h_ #define _STATIC_SEQUENTIAL_QUEUE_h_#include <iostream> #include <stdio.h> #include <stdlib.h> using namespace std;/********** 循環隊列的類型定義 **********/ #define MAXSIZE 6typedef int ElemType; typedef struct {ElemType data[MAXSIZE];int front;int rear; } SqQueue;/********** 循環隊列主要操作的實現 **********/ /** Function: 初始化操作* ----------------------------* 初始化一個空隊列Q。*/ void InitQueue(SqQueue &Q){Q.front = 0;Q.rear = 0; }/** Function: 銷毀隊列操作* ----------------------------* 靜態數組的銷毀不需要手動執行,因此無需銷毀操作。*/ void DestroyQueue(SqQueue &Q){}/** Function: 清空隊列操作* ----------------------------* 將Q清為空隊列。*/ void ClearQueue(SqQueue &Q){Q.rear = Q.front; // 將隊列設置為空 }/** Function: 判空操作* ----------------------------* 判斷隊列Q是否為空,若為空則返回true,否則返回false。*/ bool QueueEmpty(SqQueue Q){return Q.front==Q.rear; }/** Function: 求隊長操作* ----------------------------* 返回隊列Q的元素個數,即隊列的長度。*/ int QueueLength(SqQueue Q){return ((Q.rear+MAXSIZE-Q.front) % MAXSIZE); }/** Function: 讀取隊首元素操作* ----------------------------* 讀取隊首元素,若隊列非空,則用e返回隊首元素。*/ bool GetHead(SqQueue Q, ElemType &e){if (QueueEmpty(Q)){return false;}e = Q.data[Q.front];return true; }/** Function: 入隊操作* ----------------------------* 若隊列Q未滿,則將e插入,使之成為新隊尾。*/ bool EnQueue(SqQueue &Q, ElemType e){if (QueueLength(Q)<MAXSIZE-1){ // 注意要預留一個空位置Q.data[Q.rear] = e;Q.rear = (Q.rear+1) % MAXSIZE;return true;} else {printf("Out of space!\n"); return false;} }/** Function: 出隊操作* ----------------------------* 若隊列Q非空,則彈出隊首元素,并用e返回。*/ bool DeQueue(SqQueue &Q, ElemType &e){if (GetHead(Q, e)){Q.front = (Q.front+1) % MAXSIZE;return true;}return false; }/** Function: 輸出操作* ----------------------------* 按從隊首頭到隊尾的順序輸出。*/ void Print(SqQueue Q){int len = QueueLength(Q);for (int i=Q.front;i<Q.front+len;i++){printf("%d <-", Q.data[i]);}printf("\n"); }#endif // _STATIC_SEQUENTIAL_QUEUE_h_動態循環隊列
/** Filename: DynamicSqQueue.h* -----------------------* 使用動態數組實現循環隊列。*/#ifndef _DYNAMIC_SEQUENTIAL_QUEUE_h_ #define _DYNAMIC_SEQUENTIAL_QUEUE_h_#include <iostream> #include <stdio.h> #include <stdlib.h> using namespace std;/********** 動態循環隊列的類型定義 **********/ #define INITSIZE 6 #define INCREMENT 6typedef int ElemType; typedef struct {ElemType *data;int front;int rear;int size; // 隊列的總容量(包含預留給rear指針的空位置) } SqQueue;/********** 動態循環隊列主要操作的實現 **********/ /** Function: 初始化操作* ----------------------------* 初始化一個空隊列Q。*/ void InitQueue(SqQueue &Q){Q.data = new ElemType[INITSIZE];Q.front = 0;Q.rear = 0;Q.size = INITSIZE; }/** Function: 銷毀隊列操作* ----------------------------* 銷毀隊列,并釋放隊列Q占用的存儲空間。*/ void DestroyQueue(SqQueue &Q){Q.rear = 0;Q.front = 0; delete[] Q.data; }/** Function: 清空隊列操作* ----------------------------* 將Q清為空隊列。*/ void ClearQueue(SqQueue &Q){Q.rear = Q.front; // 將隊列設置為空 }/** Function: 判空操作* ----------------------------* 判斷隊列Q是否為空,若為空則返回true,否則返回false。*/ bool QueueEmpty(SqQueue Q){return Q.front==Q.rear; }/** Function: 求隊長操作* ----------------------------* 返回隊列Q的元素個數,即隊列的長度。*/ int QueueLength(SqQueue Q){return ((Q.rear+Q.size-Q.front) % Q.size); }/** Function: 讀取隊首元素操作* ----------------------------* 讀取隊首元素,若隊列非空,則用e返回隊首元素。*/ bool GetHead(SqQueue Q, ElemType &e){if (QueueEmpty(Q)){return false;}e = Q.data[Q.front];return true; }/** Function: 入隊操作* ----------------------------* 若隊列Q未滿,則將e插入,使之成為新隊尾,* 否則先擴容,再插入。*/ bool EnQueue(SqQueue &Q, ElemType e){int oldSize = Q.size;if (QueueLength(Q)>=Q.size-1){ // 需要擴容Q.data = (ElemType*) realloc (Q.data, (Q.size+INCREMENT) * sizeof(ElemType));Q.size += INCREMENT;printf("Increment\n");} // 擴容完成后隊列可能出現斷裂if (Q.front > Q.rear){int tmp1 = 0; // 兩個臨時指針分別指向兩個斷裂處int tmp2 = oldSize; while (tmp1!=Q.rear){Q.data[tmp2++] = Q.data[tmp1++];}Q.rear = tmp2;}// 執行插入Q.data[Q.rear] = e;Q.rear = (Q.rear+1) % Q.size;return true; }/** Function: 出隊操作* ----------------------------* 若隊列Q非空,則彈出隊首元素,并用e返回。*/ bool DeQueue(SqQueue &Q, ElemType &e){if (GetHead(Q, e)){Q.front = (Q.front+1) % Q.size;return true;}return false; }/** Function: 輸出操作* ----------------------------* 按從隊首頭到隊尾的順序輸出。*/ void Print(SqQueue Q){int len = QueueLength(Q);for (int i=Q.front;i<Q.front+len;i++){printf("%d <-", Q.data[i]);}printf("\n"); }#endif // _DYNAMIC_SEQUENTIAL_QUEUE_h_循環隊列檢測程序
/** Filename: SqQueueTest.cpp* -----------------------* 檢測循環隊列。*/// 引用靜態循環隊列 #include "StaticSqQueue.h"// 引用動態循環隊列 // #include "DynamicSqQueue.h"int main(){SqQueue Q;InitQueue(Q);int n;ElemType e;char helpInfo[] ="*****************************\n""Sequential Queue check: \n""\t-2-Quit\n""\t1-EnQueue\n""\t2-DeQueue\n""\t3-Empty check\n""\t4-Get Length\n""\t5-Get head\n""\t6-Clear\n""\t7-Print\n""*****************************\n";while (n!=-2){printf(helpInfo);scanf("%d", &n);switch(n){case 1:printf("Enter the new value: ");scanf("%d", &e);EnQueue(Q, e);break;case 2:if (DeQueue(Q, e)){printf("The first value %d is dequeued.\n", e);} else {printf("The Queue is empty.\n");}break;case 3:if (QueueEmpty(Q)){printf("The Queue is empty.\n");} else {printf("The Queue is not empty.\n");}break;case 4:printf("The length of Queue is: %d\n", QueueLength(Q));break;case 5:if (GetHead(Q, e)){printf("The Head value is: %d\n", e);} else {printf("The Queue is empty.\n");}break;case 6:ClearQueue(Q);printf("All cleared.\n");break;case 7:printf("Queue is: ");Print(Q);break;}}DestroyQueue(Q);return 0; }單向鏈隊列
單向鏈隊列的實現
/** Filename: SingleLinkedQueue.h* -----------------------* 使用單鏈表實現隊列。*/#ifndef _SIGNLE_LINKED_LIST_h_ #define _SIGNLE_LINKED_LIST_h_#include <iostream> #include <stdio.h> #include <stdlib.h> using namespace std;/********** 鏈隊列的類型定義 **********/ typedef int ElemType;typedef struct LNode{ ElemType data; // 數據域struct LNode *next; // 指針域 } LNode;typedef struct {LNode *front, *rear; } LinkedQueue;/********** 鏈隊列主要操作的實現 **********/ /** Function: 初始化操作* ----------------------------* 初始化一個空隊列Q。*/ void InitQueue(LinkedQueue &Q){Q.front = new LNode; // 頭結點Q.front->next = NULL;Q.rear = Q.front; }/** Function: 清空隊列操作* ----------------------------* 將Q清為空隊列。*/ void ClearQueue(LinkedQueue &Q){LNode *tmp = Q.front->next;while (tmp!=NULL){Q.front->next = tmp->next;delete tmp;tmp = Q.front->next;}Q.rear = Q.front; }/** Function: 銷毀隊列操作* ----------------------------* 銷毀隊列,并釋放隊列Q占用的存儲空間。*/ void DestroyQueue(LinkedQueue &Q){ClearQueue(Q);delete Q.front; }/** Function: 判空操作* ----------------------------* 判斷隊列Q是否為空,若為空則返回true,否則返回false。*/ bool QueueEmpty(LinkedQueue Q){return Q.front==Q.rear; }/** Function: 求隊長操作* ----------------------------* 返回隊列Q的元素個數,即隊列的長度。*/ int QueueLength(LinkedQueue Q){int count=0;LNode *tmp = Q.front->next;while (tmp!=NULL){count++;tmp = tmp->next;}return count; }/** Function: 讀取隊首元素操作* ----------------------------* 讀取隊首元素,若隊列非空,則用e返回隊首元素。*/ bool GetHead(LinkedQueue Q, ElemType &e){if (QueueEmpty(Q)){return false;}e = Q.front->next->data;return true; }/** Function: 入隊操作* ----------------------------* 將e插入,使之成為新隊尾,* 注意插入完成后要更新尾指針。*/ bool EnQueue(LinkedQueue &Q, ElemType e){LNode *n = new LNode; // 創建新的隊尾結點n->data = e;n->next = NULL;Q.rear->next = n; // 將n加入隊尾Q.rear = n; // 更新尾指針return true; }/** Function: 出隊操作* ----------------------------* 若隊列Q非空,則彈出隊首元素,并用e返回。*/ bool DeQueue(LinkedQueue &Q, ElemType &e){if (QueueEmpty(Q)){return false;}LNode *tmp = Q.front->next;e = tmp->data;if (tmp==Q.rear){ // 如果隊列只含有一個元素,刪除后rear指針也會丟失Q.rear=Q.front; // 因此可以提前將rear指向front}Q.front->next = tmp->next;delete tmp;return true; }/** Function: 輸出操作* ----------------------------* 按從隊首頭到隊尾的順序輸出。*/ void Print(LinkedQueue Q){LNode *tmp = Q.front->next;while (tmp!=NULL){printf("%d ->", tmp->data);tmp = tmp->next;}printf("\n"); }#endif // _SIGNLE_LINKED_LIST_h_總結
以上是生活随笔為你收集整理的十、【栈和队列】队列的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 七、【栈和队列】栈
- 下一篇: 八、【栈和队列】栈的应用