提要鉤玄:本文主要介紹佇列的結構、基本原理及操作,涉及到兩種實作:順序佇列和鏈佇列,
1. 什么是佇列?
先舉一個日常例子,排隊買飯,
大家按先來后到的順序,在視窗前排隊買飯,先到先得,買完之后走開,輪到下一位買,新來的人排在隊尾,不能插隊,
可見,上面的“隊”的特點是只允許從一端進入,從另一端離開,
這樣的一個隊,放在資料結構中就是“佇列”,
首先,佇列是一個線性表,所以它具有線性表的基本特點,
其次,佇列是一個受限的線性表,受限之處為:只允許從一端進入佇列,從另一端離開,
根據以上特點,可以畫出示意圖:
出隊元素 1,入隊元素 4 之后:
下面是幾個相關名詞:
- 入隊:進入佇列,即向佇列中插入元素
- 出隊:離開佇列,即從佇列中洗掉元素
- 隊頭:允許出隊(洗掉)的一端
- 隊尾:允許入隊(插入)的一端
- 隊頭元素:佇列中最先入堆疊的元素
- 隊尾元素:佇列中最后入堆疊的元素
我們可以直接將隊頭元素看作隊頭,隊尾元素看作隊尾,(這些名詞概念,有所理解即可,不必細究)
佇列的重要特性是在隊尾進行入隊操作,在隊頭進行出隊操作,所以上圖元素的入隊順序為:1、2、3,出隊順序為:1、2、3,也即,先入隊的先出隊(First In First Out, FIFO),后入隊的后出隊(Last In Last Out, LILO).
總結一下,佇列是一種只允許在一端進行插入操作,在另一端進行洗掉操作的先入先出的受限的線性表,
2. 佇列的實作思路
和堆疊一樣,佇列也可以有兩種實作方式:陣列實作的順序佇列和鏈表實作的鏈佇列,
2.1. 陣列實作——順序佇列
一個用陣列實作的順序佇列如下圖所示:
可以看到,要實作一個順序佇列,我們需要以下結構:
- 存盤資料的陣列 ——
data[] - 表示佇列的最大容量的值 ——
MAXSIZE - 標識隊頭端的隊頭下標 ——
front - 標識隊尾端的隊尾下標 ——
rear
front 和 rear 會隨著入隊和出隊操作而變化,為了方便起見,我們規定在非空佇列中,隊尾下標是隊尾元素的下一個元素的下標,
了解了結構之后,我們可以很容易使用 C 語言的結構體實作它:
#define MAXSIZE 5 //順序佇列的最大存盤容量
/*順序佇列的結構體*/
typedef struct {
int data[MAXSIZE];
int front; //隊頭下標
int rear; //隊尾下標
} QueueArray;
2.2. 鏈表實作——鏈佇列
我們使用帶頭節點的單鏈表來實作佇列,如下圖所示:
可以看到,要實作一個鏈佇列,需要以下結構:
- 單鏈表的基本單元結點 ——
QueueNode- 存盤資料的資料域 ——
data - 指向下一個結點的指標域 ——
next
- 存盤資料的資料域 ——
- 指向鏈表的頭指標 ——
head - 標識隊頭端的隊頭指標 ——
front - 標識隊尾端的隊尾指標 ——
rear
其中,頭指標 head 和隊頭指標 front 都指向了單鏈表的第一個結點,所以這個指標可以合二為一,隊頭指標即頭指標,
如此一來,我們可以借助鏈表的尾插法實作佇列的入隊操作,借助鏈表的頭刪法實作佇列的出隊操作,
搞清了結構,用結構體實作如下:
/*單鏈表的結點的結構體*/
typedef struct QueueNode {
int data; //資料域
struct QueueNode *next; //指標域
} QueueNode;
/*鏈佇列的結構體*/
typedef struct {
QueueNode *front; //隊頭指標
QueueNode *rear; //隊尾指標
} QueueLink;
3. 佇列的狀態
3.1. 順序佇列(問題版)
【空佇列】:空佇列中沒有元素,此時,隊頭下標和隊尾下標均為 0,即front = rear = 0:
【非空非滿佇列】:佇列不是空佇列且有剩余空間:
【滿佇列】:順序佇列分配的固定空間用盡,沒有多余空間,不能再插入元素,此時 front = 0,rear = MAXSIZE:
從上圖中可以看出,非空佇列的隊尾下標 rear 始終是隊尾元素的下一個元素的下標,
3.2. 假滿佇列
以上是用陣列實作的順序佇列的三種狀態,但上圖中三種佇列是存在問題的,那就是佇列的存盤問題!
先再次明確佇列的兩條重要特性:
- 佇列只允許在隊頭洗掉元素,在隊尾插入元素
- 我們規定:
front是隊頭元素的下標,rear是隊尾元素的下標,二者會隨著出隊和入隊操作而變化
由于上面的三幅圖中 front 都在下標 0 處,所以不容易看出問題,請看下面的程序圖:
簡單用文字描述以下上述程序:
圖1:空佇列
圖2:進隊 3 個元素:1、2、3
圖3:出隊 2 個元素:1、2
圖4:入隊 2 個元素:4、5
到此為止,一切正常,
圖5:入隊 1 個元素,但在圖4中 rear = 5已經超出陣列的最大范圍,所以圖5入隊一個元素會報錯,這個佇列不能再插入元素了,
圖5的佇列滿了嗎?沒滿!能繼續插入元素嗎?不能!有剩余空間卻不能用,這就好比有空房的酒店不讓客戶入住,這叫不會做生意,
滿佇列的是空間用盡,不能再插入元素的佇列,雖然圖5的佇列也不能繼續插入元素了,但它還有剩余空間,所以這樣的佇列還不能稱之為滿佇列,可稱之為假滿佇列,
之所以假滿佇列存在問題,是因為順序佇列的空間是有限的,通過若干入隊操作之后,我們的 rear “跑”到陣列外從而導致越界了,
明明才存盤了一個元素,卻因為假滿,整個佇列不能再存盤了,這樣的佇列肯定不是合格的資料結構,
怎么解決呢?報錯是 rear 越界導致,而佇列的前大部分都是空閑的,所以當 rear 越界時,我們可不可以將其移動到下標 0 處呢?
顯然是可以的,這樣就構成了一個“回圈”,我們稱這種 front 和 rear可以回圈利用的佇列為回圈佇列,
3.3. 回圈佇列
為了突出“回圈”二字,我們將這種順序佇列畫成一個圓:
回圈佇列的 rear 和 front 能夠在佇列中一圈一圈地轉,像鐘表的時針和分針一樣,不會再出現不能利用的空間了,
順序佇列的形式從“直的”變成這種可回圈的之后,對于狀態的判斷也改變了,
【空佇列】:佇列中沒有元素,如上圖,
請注意,空佇列的條件并不是 front = rear = 0,比如一個空佇列經過 3 次入隊和 3 次出隊操作后仍為空佇列:
所以,回圈佇列為空佇列時,條件應該為 front = rear
【滿佇列】:佇列中沒有空閑空間
上圖是一個最大容量為 8 的空佇列,入隊 7 個元素后,佇列中還剩 1 個空閑位置,如果此時我們再入隊 1 個元素:
此時佇列中確實沒有空閑空間了,但注意,此時佇列滿足了 rear = front ,但滿足 rear = front的佇列不應該是空佇列嗎?
這就產生誤會了,
不如我們退一步海闊天空,少用一個元素,借此來消除誤會,如下圖,規定這樣是一個滿佇列,
我們規定,front 出現在 rear 的下一個位置時,佇列為滿佇列,
比如在上圖的滿佇列中, front = 3 在 rear = 2 的下一個位置,
所以佇列為滿佇列的判定條件為:rear + 1 = front,但這的條件是不準確的,
因為回圈佇列中的 front 和 rear 都是回圈使用的,就像鐘表的時針一樣,所以我們僅根據下標的大小來判斷位置是不合理的,下面兩個均是滿佇列,右圖不滿足rear + 1 = front:
就像鐘表的時針滿 12 歸零一樣,front 和 rear 也應該滿某個數后歸零,這個數就是 MAXSIZE,
比如 rear = 7 時,如果按平常做法來 ,下一步應該是 rear = 8,但在這里,我們讓其歸零,所以下一步應該是 rear = 0,
用數學公式來表示上面的歸零程序就是:rear % MAXSIZE
所以滿佇列的判斷條件應該為:(rear + 1) % MAXSIZE = front,
【非空非滿佇列】很好理解,不再贅述,
3.4. 鏈佇列
我們使用帶頭結點的單鏈表來實作鏈佇列,
【空佇列】:即一個空鏈表,此時隊頭指標(兼鏈表頭指標)和隊尾指標均指向頭結點,
【非空佇列】:不像順序佇列那樣有空間的限制,鏈佇列的空間是不受限制的(只要你的記憶體足夠大),所以自然不存在“滿佇列”“回圈佇列”的概念,
4. 初始化
在進行佇列的操作前,應該先將其初始化出來,即初始化一個空佇列出來,
4.1. 順序佇列
將佇列的隊頭下標和隊尾下標置為 0 即可,
/**
* 初始化順序佇列:將隊頭下標和隊尾下標置為0
* queue: 指向佇列的指標
*/
void init(QueueArray *queue)
{
queue->front = 0;
queue->rear = 0;
}
4.2. 鏈佇列
創造出頭結點,然后將隊頭指標和隊尾指標均指向頭結點即可,
/**
* 初始化鏈佇列:將隊頭指標和隊尾指標指向頭結點
*/
void init(QueueLink *queue)
{
//創造頭結點
QueueNode *head_node = create_node(0);
//隊頭指標 隊尾指標指向頭結點
queue->front = head_node;
queue->rear = head_node;
}
5. 入隊操作
入隊操作只允許元素從隊尾進,
5.1. 順序佇列
前面我們規定,順序佇列的隊尾下標為隊尾元素的下一個元素,所以直接將待入隊元素放入隊尾下標處,然后隊尾下標“加一”,(注意:回圈佇列中的加一要對 MAXSIZE 取模)
/**
* 入隊操作
* queue: 指向佇列的指標
* elem: 入隊的資料
* return: 0失敗,1成功
*/
int en_queue(QueueArray *queue, int elem)
{
//判斷佇列是否已滿
if ((queue->rear + 1) % MAXSIZE == queue->front) {
printf("佇列已滿,無法繼續入隊,\n");
return 0;
}
//元素入隊
queue->data[queue->rear] = elem;
//隊尾下標加一
queue->rear = (queue->rear + 1) % MAXSIZE;
return 1;
}
5.2. 鏈佇列
鏈佇列的入隊操作本質是單鏈表的尾插法:
/** * 入隊操作
* queue: 指向佇列的指標
* elem: 入隊的資料
*/
void en_queue(QueueLink *queue, int elem)
{
//創造新結點
QueueNode *new = create_node(elem);
//入隊(尾插法)
queue->rear->next = new;
queue->rear = new;
}
6. 出隊操作
出隊操作只允許元素從隊頭出,
6.1. 順序佇列
將隊頭下標處的元素出隊,然后將隊頭下標“加一”(對 MAXSIZE 取模),
/**
* 出隊操作
* queue: 指向佇列的指標
* elem: 指向保存出隊資料的變數
* return: 0失敗,1成功
*/
int de_queue(QueueArray *queue, int *elem)
{
//判讀佇列是否為空
if (queue->front == queue->rear) {
printf("佇列空,無元素可出,\n");
return 0;
}
//元素出隊
*elem = queue->data[queue->front];
//隊頭下標加一
queue->front = (queue->front + 1) % MAXSIZE;
return 1;
}
6.2. 鏈佇列
鏈佇列的出隊操作本質上是單鏈表的頭刪法,注意,如果出隊的是佇列中最后一個元素,需要在出隊后,將隊尾指標重新指向頭結點,重新形成空佇列,
/**
* 出隊操作
* queue: 指向佇列的指標
* elem: 指向保存變數的指標
* return: 0失敗,1成功
*/
int de_queue(QueueLink *queue, int *elem)
{
//判讀佇列是否為空
if (queue->front == queue->rear) {
printf("佇列空,無元素可出,\n");
return 0;
}
QueueNode *front_node = queue->front->next; //隊頭元素
//保存資料
*elem = front_node->data;
//隊頭元素出隊(頭刪法)
queue->front->next = front_node->next;
//如果元素出完,隊尾指標重新指向頭結點
if (front_node == queue->rear)
queue->rear = queue->front;
free(front_node);
}
7. 遍歷操作
這里以列印整個佇列為例,介紹如何遍歷佇列,
順序佇列有隊頭下標和隊尾下標,鏈佇列有隊頭指標和隊尾指標,我們要做的就是借助一個臨時變數,從隊頭下標逐個遍歷到隊尾下標即可,
7.1. 順序佇列
借助臨時變數 i,從隊頭下標開始逐個“加一”直到隊尾下標結束,
開始標志為:i = front
加一操作為:i = (i + 1) % MAXSIZE
結束標志為:i % MAXSIZE = rear
/**
* 列印佇列
*/
void output(QueueArray queue)
{
int i = queue.front;
while (i % MAXSIZE != queue.rear) {
printf("%d ", queue.data[i]);
i = (i + 1) % MAXSIZE;
}
printf("\n");
}
如何計算順序佇列的長度?當然你可以遍歷佇列然后借助計數變數來存盤長度,這樣比較麻煩,因為順序佇列是使用陣列實作的,所以順序佇列的長度我們可以直接根據下標計算出來,
如果是一個非回圈佇列,那很簡單,直接 rear - front 就是佇列的長度了,
但回圈佇列不能這樣直接減了,因為 rear 和 front 之間的位置關系是不確定的,
左圖 rear < front,我們可以將其長度看成兩部分組成:
- 下標 0 到
rear,長度為rear - 0 - 下標
MAXSIZE - 1到rear,長度為MAXSIZE - front
所以長度為 rear - front + MAXSIZE
為了滿足右圖 rear > front 的情況,如果按照上式,則此時多加了一個 MAXSIZE,所以需要對其再對 MAXIZE 取余,
所以回圈佇列的長度為 (rear - front + MAXSIZE) % MAXSIZE(空佇列也滿足),
7.2. 鏈佇列
借助指標 p 從隊頭元素遍歷至隊尾元素即可,
/**
* 列印佇列
*/
void output(QueueLink *queue)
{
QueueNode *p = queue->front->next; //p指向隊頭元素
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
以上就是佇列的基本原理及操作,
完整代碼請移步至 GitHub | Gitee 獲取
如有錯誤,還請指正,
如果覺得寫的不錯,可以點個贊和關注,后續會有更多資料結構和演算法相關文章,
【推薦閱讀】
- 【資料結構】用詳細圖文把「堆疊」搞明白(原理篇)
- 詳解 | 寫完這篇文章我終于搞懂鏈表了
- 如何掌握 C 語言的一大利器——指標?
- 用圖和代碼讓你搞懂順序結構線性表
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/273156.html
標籤:其他
上一篇:即時訊息技術剖析與實戰
下一篇:Z-score模型

