隊列(queue)是一種採用先進先出(FIFO)策略的抽象數據結構,它的想法來自於生活中排隊的策略。顧客在付款結賬的時候,按照到來的先後順序排隊結賬,先來的顧客先結賬,後來的顧客後結賬。
隊列實現
同棧的實現一樣,隊列的實現也有數組實現和鏈表實現兩種方式。
數組實現
先來看看數組實現的方法。棧使用top變量記錄棧頂的位置,而隊列則使用front和rear分別隊列第一個元素和最後一個元素的位置。
#define SIZE 20
typedef struct queue
{
int arr[SIZE];
int front;
int rear;
} Queue;
入隊、出隊操作很簡單。入隊時,通過rear的位置判斷隊列是否已滿。如果沒有滿,則將rear後移一位,將新元素放置在rear所在位置。出隊時,也可以通過rear的位置判斷隊列是否爲空。如果不空,則只需將front後移一位即可。 獲取隊頭元素,判斷隊列不空,則只需返回front指向位置的元素即可。
哇塞!好簡單啊!這樣就完成了隊列的實現啊!太天真了。。。想一下,通過出隊操作將數據彈出隊列後,front之前的空間還能夠再次得到嗎?不能。所以使用普通數組實現隊列,就再也不能使用front之前的空間了,這會導致大量空間丟失。
循環數組
爲了解決這個問題,將普通數組換成循環數組。在循環數組中,末尾元素的下一個元素不是數組外,而是數組的頭元素。這樣就能夠再次使用front之前的存儲空間了。
哇塞!有解決了這個問題。可以開始碼代碼了!且慢,在實現修改入隊和出隊操作之前,再深思一步,這時是否還能簡單通過判斷rear的位置來判斷隊列空或滿呢?當然不可以。那是否可以考慮front和rear之間的位置關係呢?考慮如下兩種邊界情況:
在這兩種情況下,rear都在front前一個位置,無法判斷此時隊列滿還是空。爲了解決這個問題,通常採用的最簡單的方法就是,使用一個counter記錄隊列中元素個數。修改隊列定義爲下:
#define SIZE 20
typedef struct queue
{
int arr[SIZE];
int front;
int rear;
int counter;
} Queue;
Init(Queue *q)
{
q->front = 0;
q->rear = -1;
q->counter = 0;
}
bool IsFull(Queue *q)
{
return (q->counter >= SIZE);
}
void EnQueue(Queue *q, int val)
{
if(IsFull(q))
return;
q->rear = (q->rear + 1) % SIZE;
q->arr[q->rear] = val;
q->counter++;
}
bool IsEmpty(Queue *q)
{
return (q->counter <= 0);
}
void DeQueue(Queue *q)
{
if (IsEmpty(q))
return;
q->front = (q->front + 1) % SIZE;
q->counter--;
}
//get the front element from queue
int Front(Queue *q)
{
if (IsEmpty(q))
return;
return q->arr[q->front];
}
另外一種方法是,在數組中空出一個位置,用以標記隊列是否已滿。
bool IsFull(Queue *q)
{
return ((q->rear + 2) % SIZE == q->front);
}
bool IsEmpty(Queue *q)
{
return ((q->rear + 1) % SIZE == q->front);
}
有時候,rear表示隊尾元素的下一個位置,即表示即將插入元素的位置。
此時,判斷隊列的操作應該修改如下:
#define SIZE 20
typedef struct queue
{
int arr[SIZE];
int front;
int rear;
} Queue;
Init(Queue *q)
{
q->front = 0;
q->rear = 0;
}
bool IsFull(Queue *q)
{
return ((q->rear + 1) % SIZE == q->front);
}
void EnQueue(Queue *q, int val)
{
if(IsFull(q))
return;
q->arr[q->rear] = val;
q->rear = (q->rear + 1) % SIZE;
}
bool IsEmpty(Queue *q)
{
return (q->rear == q->front);
}
void DeQueue(Queue *q)
{
if (IsEmpty(q))
return;
q->front = (q->front + 1) % SIZE;
}
int Front(Queue *q)
{
if (IsEmpty(q))
return;
return q->arr[q->front];
}
鏈表實現
隊列的數組實現比較麻煩,需要考慮各種邊界情況,所以通常使用鏈表形式來實現隊列。
使用單向鏈表來實現鏈式隊列,鏈式隊列中存儲front和rear即可。
typedef struct node
{
int val;
struct node *next;
}Node;
typedef struct queue
{
Node *front;
Node *rear;
};
爲了方便實現,鏈式隊列中的front表示鏈表的頭節點,而front的next才表示隊頭。鏈式隊列的入隊和出隊操作如下圖所示。從圖中可以看出鏈式隊列的實現採用尾進頭出的策略。
在鏈式隊列中,不需要考慮隊列是否已滿,只要內存足夠就可以一直分配空間。而當front和rear都指向頭節點的時候,則表示此時隊列爲空。
bool IsEmpty(Queue *q)
{
return (q->front == q->rear);
}
入隊時移動rear指向最後一個元素即可。
Node* CreateNode(int val)
{
Node *newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL)
return NULL;
else {
newNode->val = val;
newNode->next = NULL;
return newNode;
}
}
void EnQueue(Queue *q, int val)
{
Node *newNode = CreateNode(val);
if (newNode == NULL)
return;
else {
q->rear->next = newNode;
q->rear = q->rear->next;
}
}
出隊時,不需要移動front指針,只需將其next指針指向下一個next元素即可。q->front->next = q->front->next->next;
但是需要考慮一種特殊情況,即當隊列中只剩下一個元素時,還需要使rear指針重新指向頭節點。
int Front(Queue *q)
{
if (IsEmpty(q))
return -1;
else
return (q->front->next->val);
}
void DeQueue(Queue *q)
{
if (IsEmpty(q))
return;
else
{
Node *deNode = q->front->next;
q->front->next = deNode->next;
if (q->rear == deNode) //only one element
q->rear = q->front;
free(deNode);
}
}