棧和佇列

Whip發表於2019-06-10

0、引入

通常來說,決定採用何種方式來儲存資料是非常重要的,這樣便於稍後檢索資料時,資料會自動按照某種規定的順序給出。用於檢索資料的一種常用結構稱為棧,它檢索元素的順序與儲存元素的順序相反。例如:一個記錄函式呼叫軌跡的資料塊。這些資料塊稱為活躍記錄。有一個函式集{f1, f2, f3},其中f1呼叫f2,f2呼叫f3,每次當函式呼叫發生時,程式就會分配空間來記錄此啟用資訊。這些記錄會一直存在直到相應的函式返回。因為函式呼叫與函式返回是一個相反的過程,所以活躍記錄的獲取與釋放的順序也是相反的。用於檢索資料的另一種常用結構稱為佇列,它是按照元素到達的先後順序來釋放元素的。例如:我們可能有一堆事情要做,這時我們會按順序做第一件事,接著第二件……直到最後一件事完成。棧和佇列就是這樣一種常見而簡單的資料結構。

簡單來說,

棧:按照後進先出(LIFO)的順序儲存和檢索資料的高效資料結構,它檢索元素的順序與儲存元素的順序相反。

佇列:按照先進先出(FIFO)的順序儲存和檢索資料的高效資料結構,它按照儲存元素的先後順序檢索元素。


1、棧的描述

棧的一個顯著特點是它按照後進先出(LIFO)的方式儲存和刪除元素。這意味著,最後一個存入棧中的元素將會第一個被刪除。我們可以把棧形象地看做一筒網球。當往筒裡放球時,球從筒底向上排到筒口;當從筒裡拿球時,球從筒口往下依次被拿出,直到筒底的球最後一個被拿出。並且,如果我們想拿到處於筒底的那個球,那麼就必須把筒底那個球之上的球全部拿出才行。在計算機中,要把元素儲存到棧中,就“壓入”元素;要刪除棧中的元素,就“彈出”元素(見下圖)。有時候,可以通過檢查棧頂元素(而不是實際刪除它)來獲取元素的某些資訊。

棧示圖


2、棧的介面定義

stack_init

——————

void stack_init(Stack *stack, void (*destroy)(void *data));

返回值:無

描述:初始化由stack指定的棧。在對棧進行其他操作之前,必須呼叫初始化函式。引數destroy是一個函式指標,通過呼叫destroy來釋放動態分配的記憶體空間。例如:如果一個棧包含用malloc動態分配記憶體的資料,那麼此棧銷燬時,destroy會呼叫fee來釋放記憶體空間。當一個結構化資料包含若干動態分配記憶體的資料成員時,destroy應該指向一個使用者自定義的函式來釋放每個動態分配的資料成員和結構本身的記憶體空間。如果不需要釋放棧中的資料,那麼destroy應該指向NULL。

複雜度:O(1)


stack_destroy

——————

void stack_destroy(Stack *stack);

返回值:無

描述:銷燬由stack指定的棧。在呼叫stack_destroy之後不再允許進行其他操作,除非再次呼叫stack_init。stack_destroy會刪除棧中的所有元素,如果傳遞給stack_init的引數destroy不為NULL,每次移除一個元素時,都要呼叫destroy一次。

複雜度:O(n),n為棧中元素的個數。


stack_push

——————

int stack_push(Stack *stack, const void *data);

返回值:如果元素入棧成功則返回0;否則返回-1。

描述:向stack指定的棧中壓入一個元素。新元素包含一個指向data的指標,因此只要元素仍然在棧中,data引用的記憶體就一直有效。與data相關的儲存空間將由函式的呼叫者來管理。


stack_pop

——————

int stack_pop(Stack *stack, void **data);

返回值:如果元素出棧成功則返回0;否則返回-1。

描述:從stack指定的棧中彈出一個元素。返回時,data指向已彈出元素中儲存的資料。與data相關的儲存空間將由函式的呼叫者來管理。

複雜度:O(1)


stack_peek

——————

void *stack_peek(const Stack *stack);

返回值:棧頂部元素中儲存的資料;如果棧為空,則返回NULL。

描述:獲取stack指定的棧頂部元素中儲存的資料的巨集。

複雜度:O(1)


stack_size

——————

int stack_size(const Stack *stack);

返回值:棧中元素的個數。

描述:獲取stack指定的棧中元素個數的巨集。

複雜度:O(1)


3、棧的實現與分析

結構Stack是棧的資料結構。實現棧的方法有很多,其中之一是用連結串列來實現。可以通過typedef List Stack這種簡單的方法來做到這一點。這種方法不僅簡單,而且可以使棧具有多型的特性。通俗地講,多型通常是面嚮物件語言的一種特性,它允許某種型別的物件(變數)在使用時用其他型別的物件(變數)代替。這意味著,除了使用棧本身的操作外,還可以使用連結串列中的操作,這是因為棧本身就是一種連結串列,它與連結串列有相同的特性。因此,很多時候可以像使用連結串列一樣使用棧。

這裡有一個例子,假設要遍歷一個棧中的元素,通過這種方式來顯示棧中的元素或者判斷元素是否屬於這個棧。為此,首先獲取連結串列的頭元素list_head,然後用list_next遍歷它。在這裡僅僅使用了棧的操作,將元素以一個一個地彈出,檢查元素,並將它們臨時壓入另外一個棧中。在處理完所有元素之後,要重新建立原始棧,可以將臨時棧中的元素一個一個彈出,並將它們壓入原始棧中。但我們要清楚,這種方法並不高效,且在實際的程式中也不常見。

// 棧抽象資料型別的標頭檔案

/* stack.h */

#ifndef STACK_H
#define STACK_H

#include <stdlib.h>

#include "list.h"

/* Implement stacks as linked lists. */
typedef List Stack;

/* Public Interface */
#define stack_init list_init

#define stack_destroy list_destroy

int stack_push(Stack *stack, const void *data);

int stack_pop(Stack *stack, void **data);

#define stack_peek(stack) ((stack)->head == NULL ? NULL : (stack)->head->data)

#define stack_size list_size

#endif // STACK_H

// 棧抽象資料型別的實現

/* stack.c */

#include <stdlib.h>

#include "list.h"
#include "stack.h"

/* stack_push */
int stack_push(Stack *stack, const void *data)
{
    /* Push the data onto the stack. */
    return list_ins_next(stack, NULL, data);
}

/* stack_pop */
int stack_pop(Stack *stack, void **data)
{
    /* Pop the data off the stack. */
    return list_rem_next(stack, NULL, data);
}


stack_init

棧通過stack_init初始化,經過初始化的棧才能進行其他操作。因為棧本身就是一個連結串列,並且初始化過程相同,所以將stack_init定義成list_init。

stack_init的執行時複雜度與list_init相同,都是O(1)。


stack_destroy

棧通過stack_destroy銷燬。因為棧本身就是一個連結串列,並且銷燬過程相同,所以將stack_destroy定義成list_destroy。

stack_destroy的執行時複雜度與list_destroy相同,都是O(n),n為棧包含元素的個數。


stack_push

棧通過stack_push往棧頂壓入元素。stack_push呼叫list_ins_next方法來插入指向連結串列頭部中data的元素。

stack_push的執行時複雜度與list_ins_next相同,都是O(1)。


stack_pop

棧通過stack_pop從棧頂彈出元素。stack_pop呼叫list_rem_next方法來刪除連結串列的頭元素,並將data指向已刪除元素中的資料。

stack_pop的執行時複雜度與list_rem_next相同,都是O(1)。


stack_peek與stack_size

這是棧的兩個巨集定義,實現了棧的兩種簡單操作。stack_peek用來獲取棧頂元素的資訊而並不彈出棧頂元素;stack_size用來獲取棧的大小。這兩種操作都是通過訪問Stack結構的成員來實現的。

由於訪問結構的成員變數是一種簡單的操作,只會消耗固定的執行時間,因此,這兩種巨集的執行時複雜度為O(1)。


4、佇列的描述

佇列的一個顯著特徵是它按照先進先出(FIFO)的方式儲存和檢索元素。這意味著首先存入佇列的元素將首先被刪除。我們可以把佇列形象地看成在郵局排隊辦業務的一隊人。當新的人一個一個排到隊尾時,隊伍在不停變化。當隊伍最前面的人完成服務後,將首先離開,接著是下一個,再下一個……在計算機中,將一個元素放置到隊尾,稱為“入隊”操作;將一個元素從佇列頭刪除,稱為“出隊”操作(如下圖)。我們可以通過檢查佇列頭元素(而不是實際刪除它)來獲取元素的某些資訊。

佇列示圖


5、佇列的介面定義

queue_init

——————

void queue_init(Queue *queue, void (*destroy)(void *data));

返回值:無

描述:初始化由queue指定的佇列。在佇列進行其他操作之前,必須呼叫初始化函式。引數destroy是一個函式指標,通過呼叫destroy來釋放動態分配的記憶體空間。如果佇列中的資料不需要釋放,那麼destroy應該指向NULL。

複雜度:O(1)


queue_destroy

——————

void queue_destroy(Queue *queue);

返回值:無

描述:銷燬由queue指定的佇列。在呼叫queue_destroy之後佇列不允許進行其他操作,除非再次呼叫queue_init。queue_destroy會刪除佇列中的所有元素,同時如果queue_init中引數destroy不為NULL,會呼叫destroy釋放成員所佔用的記憶體空間。

複雜度:O(n),n為佇列中元素的個數。


queue_enqueue

——————

int queue_enqueue(Queue *queue, const void *data);

返回值:如果元素入隊成功則返回0;否則返回-1。

描述:向queue指定的佇列末尾中插入一個元素。新元素包含一個指向data的指標,因此只要元素仍然在於佇列中,data引用的記憶體就一直有效。與data相關的儲存空間將由函式的呼叫者來管理。

複雜度:O(1)


queue_dequeue

——————

int queue_dequeue(Queue *queue, void **data);

返回值:如果元素出隊成功則返回0;否則返回-1。

描述:從queue指定的佇列頭部刪除一個元素。返回時,data指向已出隊元素中儲存的資料。與data相關的儲存空間將由函式的呼叫者來管理。

複雜度:O(1)


queue_peek

——————

void *queue_peek(const Queue *queue);

返回值:佇列頭部元素中儲存的資料;如果佇列為空,則返回NULL。

描述:獲取由queue指定的佇列頭部元素中儲存資料的巨集。

複雜度:O(1)


queue_size

——————

返回值:佇列中元素的個數。

描述:獲取由queue指定的佇列元素個數的巨集。

複雜度:O(1)


6、佇列的實現與分析

結構Queue是佇列的資料結構。同棧一樣,也用typedef List Queue來定義它。

// 佇列抽象資料型別的標頭檔案

/* queue.h */

#ifndef QUEUE_H
#define QUEUE_H

#include <stdlib.h>

#include "list.h"

/* Implementation queues as linked lists. */
typedef List Queue;

/* Public Interface */
#define queue_init list_init

#define queue_destroy list_destroy

int queue_enqueue(Queue *queue, const void *data);

int queue_dequeue(Queue *queue, void **data);

#define queue_peek(queue) ((queue)->head == NULL ? NULL : (queue)->head->data)

#define queue_size list_size

#endif // QUEUE_H

// 佇列抽象資料型別的實現

/* queue.c */

#include <stdlib.h>

#include "list.h"
#include "queue.h"

/* queue_enqueue */
int queue_enqueue(Queue *queue, const void *data)
{
    /* Enqueue the data. */
    return list_ins_next(queue, list_tail(queue), data);
}

/* queue_dequeue */
int queue_dequeue(Queue *queue, void **data)
{
    /* Dequeue the data. */
    return list_rem_next(queue, NULL, data);
}


queue_init

佇列通過queue_init初始化,經過初始化的佇列才能進行其他的操作。因為佇列本身是一個連結串列,並且初始化過程相同,所以將queue_init定義成list_init。

queue_init的執行時複雜度與list_init相同,都是O(1)。


queue_destroy

佇列通過queue_destroy銷燬。因為佇列本身就是一個連結串列,並且銷燬過程相同,所以將queue_destroy定義成list_destroy。

queue_destroy的執行時複雜度與list_destroy相同,都是O(n),n為佇列包含元素的個數。


queue_enqueue

佇列通過queue_enqueue往佇列尾部插入元素。queue_enqueue呼叫list_ins_next方法來將指向data的元素插入連結串列尾部。

queue_enqueue的執行時複雜度與list_ins_next相同,都是O(1)。


queue_dequeue

佇列通過queue_dequeue從佇列頭部刪除元素。queue_dequeue呼叫list_rem_next方法來刪除連結串列的頭元素,並將data指向已刪除元素中的資料。

queue_dequeue的執行時複雜度與list_rem_next相同,都是O(1)。


queue_peek與queue_size

這是佇列的兩個巨集定義,實現佇列的兩種簡單操作。queue_peek用來獲取佇列頭元素的資訊而並不使頭元素出隊;queue_size用來獲取佇列的大小。這兩種操作都是通過訪問Queue的結構成員來實現的。

由於訪問成員變數是一種簡單的操作,只消耗固定的執行時間,所以這兩種巨集的執行時複雜度為O(1)。


7、佇列示例:事件處理

在事件驅動的應用中利用佇列來處理事件是一種常見的方法。事件驅動的應用主要遵循實時事件發生的順序來執行。例如,在Java、X或Windows中開發圖形使用者介面,應用程式的行為主要取決於鍵盤操作、滑鼠移動等一些由使用者觸發的事件。其他一些事件驅動應用的例子還包括飛機或工廠裝置中的控制系統等。

在很多事件驅動的應用中,事件可能隨時發生,因此在能夠處理這些已發生的事件之前,有序地儲存和管理這些事件是非常重要的。由於系統處理事件的順序基本上是按照事件發生的先後順序進行的,因此佇列是處理這種情況的較好方法。

下面的程式碼中列舉了兩個用於事件處理的函式:receive_event和process_event。兩個函式都用於處理包含Event型別事件的佇列。Event在event.h中定義,在此沒有列舉出來,因為每個人的Event結構體都不大相同。一個應用程式呼叫receive_event將一個將要處理的事件入隊。有很多種方式可以用來告知應用程式將有事件要進行處理,最常用的是通過硬體中斷的方式告知。當應用程式認為是時候來處理一個事件時它就會呼叫process_event函式。在process_event函式內部,事件從佇列出隊,並轉交由應用程式指定的具體的排程函式處理。排程函式作為引數dispatch傳遞給process_event。使用排程函式目的是採取適當的行動來處理事件。一般有兩種常用的排程方法:同步地處理事件,即在處理的事件未處理完成之前無法進行下一個操作;非同步地處理事件,即在事件處理的過程中,還能另外啟動程式來處理其他事件。通常非同步處理事件效率更高,但在處理主從程式之間的關係時需要特別小心,以免衝突。

receive_event的執行時複雜度為O(1),因為它只呼叫了複雜度為O(1)的佇列操作queue_enqueue。process_event的執行時複雜度取決於它所呼叫的排程函式,process_event剩下部分執行固定的時間。

// 處理事件函式的實現

/* events.c */

#include <stdlib.h>
#include <string.h>

#include "event.h"
#include "events.h"
#include "queue.h"

/* receive_event */
int receive_event(Queue *events, const Event *event)
{
    Event *new_event;

    /* Allocate space for the event. */
    if((new_event = (Event *)malloc(sizeof(Event))) == NULL)
        return -1;

    /* Make a copy of the event and enqueue it. */
    memcpy(new_event, event, sizeof(Event));

    if(queue_enqueue(events, new_event) != 0)
        return -1;

    return 0;
}

/* process_event */
int process_event(Queue *events, int (*dispatch)(Event *event))
{
    Event *event;

    if(queue_size(events) == 0)
    {
        /* Return that there are no events to dispatch. */
        return -1;
    }
    else
    {
        if(queue_dequeue(events, (void **)&event) != 0)
        {
            /* Return that an event could not be retrieved. */
            return -1;
        }
        else
        {
            /* Call a user-defined function to dispatch the event. */
            dispatch(event);
            free(event);
        }
    }

    return 0;
}



相關文章