位元詩人 [資料結構] 使用 C 語言:以連結串列 (Linked List) 為基礎的佇列 (Queue)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

佇列的抽象資料結構

佇列 (queue) 是另一種受限制的線性資料結構。其操作方式為從尾端推入,從頭端推出,是一種 FIFO (First-In, First-Out) 的資料結構。在現實生活中,佇列就像是在大賣場排隊結帳的人,先排隊的人可以先結帳。本文以串列實作佇列。

佇列

以下是佇列的抽象資料結構:

Q is a queue.

sub IsEmpty(Q): bool
(Optional) IsFull(Q): bool
(Optional) Size(Q): sz
sub Peek(Q): data
(Optional) PeekRear(Q): data
sub Enqueue(Q, data): void
sub Dequeue(Q): data

由此抽象資料結構可知,佇列至少有以下公開行為:

  • IsEmpty(Q):確認佇列是否為空
  • Peek(Q):檢視佇列頭端的資料,不改變佇列大小
  • Enqueue(Q, data):從佇列尾端推入資料,佇列長度加 1
  • Dequeue(Q):從佇列頭端推出資料,佇列長度減 1

以下方法是選擇性的:

  • IsFull(Q):檢查佇列是否已滿
  • Size(Q):回傳佇列的大小
  • PeekRear(Q):存取佇列尾端的資料,不改變佇列大小

佇列的公開界面

以下是佇列的公開界面:

#ifndef QUEUE_H
#define QUEUE_H

#ifndef __cplusplus
    #include <stdbool.h>
#endif

typedef struct queue_t queue_t;

#ifdef __cplusplus
extern "C" {
#endif

queue_t * queue_new(void);
void queue_delete(void *self);
bool queue_is_empty(const queue_t *self);
int queue_peek(const queue_t *self);
bool queue_enqueue(queue_t *self, int data);
int queue_dequeue(queue_t *self);

#ifdef __cplusplus
}
#endif

#endif  /* QUEUE_H */

除了佇列的標準操作外,這個佇列沒什麼額外的操作,算是中規中矩的佇列。

佇列的型態宣告

如同其他的串列結構,我們會用輔助性的 node_t 類別來儲存資料:

typedef struct node_t node_t;

struct node_t {
    int data;
    node_t *prev;
    node_t *next;
};

有內部節點後就可以宣告佇列型態:

typedef struct queue_t queue_t;

struct queue_t {
    node_t *head;
    node_t *tail;
};

佇列的建構函式

以下是以 C 程式碼實作的建構函式:

queue_t * queue_new(void)
{
    queue_t *q = malloc(sizeof(queue_t));
    if (!q) {
        return q;
    }

    q->head = NULL;
    q->tail = NULL;

    return q;
}

佇列的解構函式

為了要釋放佇列的內部節點,我們需要額外的兩個指標。curr 一開始指向內部節點的頭端,而 temp 是暫存值,指向 curr 所在的節點:

釋放佇列節點 (步驟一)

當我們把 curr 移往下一個節點時,就可以把 temp 所在的節點釋放掉:

釋放佇列節點 (步驟二)

重覆這個動作,就可以把所有的節點釋放掉。我們會把這個動作寫在迴圈裡,用來自動執行。

將上述動作寫到以下解構函式中,用來釋放記憶體:

void queue_delete(void *self)
{
    if (!self)
        return;

    node_t *curr = ((queue_t *) self)->head;
    while (curr) {
        node_t *temp = curr;
        curr = curr->next;
        free(temp);
    }

    free(self);
}

要先釋放佇列的內部節點,再釋放佇列物件本身。

確認佇列是否為空

以下是 IsEmpty 的 C 程式碼:

bool queue_is_empty(const queue_t *self)
{
    assert(self);

    return !(self->head) ? true : false;
}

我們的做法是檢查頭端的指標是否為空。如果在類別中有存放佇列大小的欄位,也可用該欄位檢查佇列大小。

檢視佇列頭端的資料

以下是 Peek 的 C 程式碼:

int queue_peek(const queue_t *self)
{
    assert(!queue_is_empty(self));

    return self->head->data;
}

在我們的實作中,我們會確認佇列不為空,之後直接存取頭端節點的資料即可。

將資料放入佇列

要將資料放入節點時,先建立新的資料節點 node,將 tail 節點和其相接:

將資料放入佇列 (步驟一)

再將 tail 重新把向 node 節點即可:

將資料放入佇列 (步驟二)

將上述動作 (Enqueue) 以 C 語言實作如下:

bool queue_enqueue(queue_t *self, int data)
{
    assert(self);

    node_t *node = node_new(data);
    if (!node) {
        return false;
    }

    if (!(self->head)) {
        self->head = node;
        self->tail = node;
        return true;
    }

    self->tail->next = node;
    node->prev = self->tail;
    self->tail = node;

    return true;
}

在推入資料時,要考慮佇列是否為空,兩個情境的處理方式相異。

有關 node_new 的部分可以參考以下程式碼:

static node_t * node_new(int data)
{
    node_t *node = malloc(sizeof(node_t));
    if (!node)
        return node;

    node->data = data;
    node->prev = NULL;
    node->next = NULL;

    return node;
}

將資料移出佇列

要將資料節點移出佇列時,需要使用額外的指標 curr,該指標指向 head 所在的節點:

將資料移出佇列 (步驟一)

head 移往下一個節點後,就可以釋放 curr 所在的節點:

將資料移出佇列 (步驟二)

將上述動作 (Dequeue) 寫成 C 程式碼如下:

int queue_dequeue(queue_t *self)
{
    assert(!queue_is_empty(self));

    int popped = self->head->data;

    if (self->head == self->tail) {
        free(self->head);
        self->head = NULL;
        self->tail = NULL;
    }
    else {
        node_t *curr = self->head;
        self->head = curr->next;
        free(curr);
    }

    return popped;
}

將資料推出佇列時,有三個情境:(1) 佇列為空,(2) 佇列頭端和尾端指向同一個節點,(3) 佇列包含多個節點。本範例排除情境 (1),根據情境 (2) 或 (3) 給予相對應的處理。

在演算法上的效率

根據本文的實作得到的演算法上的效率如下:

  • IsEmpty(Q): O(1)
  • Peek(Q): O(1)
  • Enqueue(Q, data): O(1)
  • Dequeue(Q): O(1)
關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。