程序猿必修課之數據結構(六)棧1

棧的定義

棧(Stack)是限定僅在表尾進行插入和刪除操作的線性表。

我們把允許插入和刪除的一端稱爲棧頂(top),另一端稱爲棧底(bottom),不含任何數據元素的棧稱爲空棧。棧又稱爲後進先出(Last In First Out)的線性表,簡稱 LIFO 結構。

從上面這兩段話,可以確定:首先棧是一個線性表,也就是說,棧元素具有線性關係,即前驅後繼關係,只不過它是一種特殊的線性表。定義中說在線性表的表尾進行插入和刪除操作,這裏表尾是指棧頂,而不是棧底。

棧的插入操作,叫做進棧,也叫壓棧、入棧。

棧的刪除操作,叫做出棧。

進棧出棧變化多端

最先進棧的元素,不一定是最後出棧的。

棧對線性表的插入和刪除的位置進行了限制,並沒有對元素進出的時間進行限制。意思就是說,當並非所有的元素都進棧的情況下,先進去的元素也可以出棧,只要保證棧頂元素出棧就可以了。

比如,現在有 3 個元素 1、2、3,可能的出棧順序有下面幾種:

  • 第一種:1、2、3 依次進棧,再 3、2、1依次出棧,顯然出棧順序爲321。
  • 第二種:1 進棧,1 出棧,2 進棧,2 出棧,3 進棧,3 出棧。出棧順序爲 123。
  • 第三種:1 進棧,2 進棧,2 出棧,1 出棧,3 進棧,3 出棧。出棧順序爲 213。
  • 第四種:1 進棧,1 出棧,2 進棧,3 進棧,3 出棧,2 出棧。出棧順序爲 132。
  • 第五種:1 進棧,2 進棧,2 出棧,3 進棧,3 出棧,1 出棧。出棧順序爲 231。

棧的抽象數據類型

我們知道,棧是一個特殊的線性表,由於它的特殊性,所以與線性表的操作會略有不同。特別是插入和刪除操作,我們分別叫它 push 和 pop。

棧的抽象數據類型

ADT 棧(stack)
Data
    同線性表,元素具有相同的類型,相鄰元素具有前驅和後繼關係。
Operation
    initStack(*S):初始化操作,建立一個空棧 S。
    destroyStack(*S):若棧存在,則銷燬它。
    clearStack(*S):清空棧。
    isEmpty(S):若棧爲空返回 true,否則返回 false。
    getTop(S, *e):若棧存在且非空,用 e 返回 S 的棧頂元素。
    push(*S, e):若棧 S 存在,插入新元素 e 到棧 S 中併成爲棧頂元素。
    pop(*S, *e):刪除棧頂元素,並且 e 返回其值。
    length(S):返回棧 S 的元素個數。
endADT

由於棧本身就是一個線性表,所以線性表的順序存儲和鏈式存儲對於棧來說,同樣適用。

棧的順序存儲結構及實現

因爲棧是線性表的特例,棧的順序存儲其實是線性表順序存儲的簡化,簡稱爲順序棧。

線性表是用數組實現的,對於棧這種只能一端插入刪除的線性表來說,下標爲 0 的一端作爲棧底比較好,因爲首元素都存在棧底,變化最小。

我們定義一個變量 top 來指示棧頂元素在數組中的位置,若棧的長度爲 stackSize,則棧頂位置 top 必須小於 stackSize。當棧中存在一個元素時,top 等於 0,因此把空棧的判定條件定爲 top = -1。

棧的結構定義

define MAXSIZE = 10

typedef struct {
    int data[MAXSIZE];
    int top;
} SqStack;

進棧操作

boolean push(SqStack *s, int e) {
    if (s->top == MAXSIZE - 1) {
        return false;
    }
    s->top++;
    s->data[s->top] = e;
    return true;
}

出棧操作

若棧不爲空,則刪除棧頂元素,並把其值賦值給 e。

boolean pop(SqStac *s, int *e) {
    if (s->top == -1)
        return false;
    *e = s->data[s->top];
    s->top--;
    return true;
}

通過進棧和出棧操作的代碼可以看出,時間複雜度都爲 O(1)。

兩棧共享空間

棧的順序存儲有一個很大的缺陷,必須先確定數組存儲空間大小,而且萬一不夠用了,還要擴容。

對於一個棧,我們只能儘量考慮周全,設計出合適大小的數組來處理,但對於兩個相同類型的棧,我們可以做到最大限度地利用事先開闢的存儲空間來進行操作。

如果我們有兩個相同類型的棧,我們爲它們各自開闢了數組空間,有可能第一個棧已經滿了,而另一個棧還有很多存儲空間。我們完全可以用一個數組來存儲兩個棧。

我們知道,數組有兩個端點,兩個棧有兩個棧底。可以讓一個棧的棧底爲數組的開始端,即下標爲 0 處,另一個棧的棧底爲數組的末端,即下標爲數組長度 n - 1 處,這樣兩個棧如果增加元素,就是兩端點向中間移動,如圖所示。

兩棧共享

top1 和 top2 是棧 1 和棧 2 的棧頂指針,只要二者的值不相等,兩個棧就可以進行入棧操作。當 top1 等於 -1 時,棧 1 爲空;當 top2等於 n 時,棧 2 爲空。當 top2 - top1 = 1 時,棧就滿了。

兩棧共享空間的代碼如下:

typedef struct {
    int data[MAXSIZE];
    int top1;
    int top2;
} SqDoubleStack;

對於兩棧共享空間的插入方法,除了要插入元素值參數外,還需要有一個判斷是棧 1 還是棧 2 的棧號參數。代碼如下:

boolean push(SqDoubleStack *s, int e, int stackNumber) {
    if (s->top2 - s->top1 == 1) {   /* 棧滿 */
        return false;
    }
    if (stackNumber == 1) {
        s->data[++s->top1] = e;
    } else if (stackNumber == 2) {
        s->data[--s->top2] = e;
    }
    return true;
}

出棧方法的代碼

boolean pop(SqDoubleStack *s, int *e, int stackNumber) {
    if (stackNumber == 1) {
        if (s->top1 == -1)
            return false;
        *e = s->data[s->top1--];
    } else if (stackNumber == 2) {
        if (s->top2 == MAXSIZE)
            return false;
        *e = s->data[s->top2++];
    }
    return true;
}

兩棧共享通常用於兩個棧的空間需求具有相反關係時,也就是一個棧增長時另一個棧在縮短。就像買賣股票一樣,一個人買入時,一定有另一個人在做賣出操作。這樣使用兩棧共享空間存儲方法纔有比較大的意義。

兩棧共享只針對兩個具有相同數據類型的棧。

棧的鏈式存儲結構

棧的鏈式存儲結構,簡稱爲鏈棧。

由於單鏈表有頭指針,而棧頂指針也是必須的,所以把棧頂放在鏈表的頭部。另外,由於已經有了棧頂在頭部,所以單鏈表中的頭結點就不需要了。

對於鏈棧來說,基本不存在棧滿的情況,因爲只要內存有空間,鏈表就可以一直添加結點。

鏈棧的結構代碼如下:

/**
 * Node結點
 */
typedef struct StackNode {
    int data;
    struct StackNode *next;
} StackNode, *pStackNode;

typedef struct LinkStack {
    pStackNode top; /* 棧頂指針 */
    int count;
} LinkStack;

棧的鏈式存儲結構——進棧操作

假設新結點 s 的值爲 e,top 爲棧頂指針,將 s->next 指向當前棧頂結點,棧元素個數加 1。

boolean push(LinkStack *s, int e) {
    pStackNode pNewNode = (pStackNode)malloc(sizeof(StackNode));
    if (NULL == newNode)
        return false;
    pNewNode->data = e;
    /* 把當前棧頂元素賦值給新結點的直接後繼 */
    pNewNode->next = s->top;
    /* 新結點作爲棧頂 */
    s->top = pNewNode;
    s->count ++;
    return true;
}

棧的鏈式存儲——出棧操作

出棧的操作步驟:先用一個變量 p 來存儲要刪除的棧頂結點,將棧頂指針 改爲 p->next,釋放 p。

boolean pop(LinkStack *s, int *e) {
    if (stackEmpty(*s)) {
        return false;
    }
    pStackNode pNode = s->top;
    *e = pNode->data;
    s->top = pNode->next;
    free(pNode);
    s->count--;
    return true;
}

上面出棧操作中,用到判斷棧是否爲空,那麼鏈棧是怎樣判斷是否爲空棧的呢?

boolean stackEmpty(LinkStack *s) {
    if (s->top->next == NULL)
        return true;
    else 
        return false;
}

總結

從鏈棧的進棧和出棧操作,可以看出時間複雜度都爲 O(1)。

對比順序棧和鏈棧,它們的時間複雜度是一樣的,對於空間性能,順序棧需要先確定一個固定的長度,可能會造成空間浪費,但是它在存取時定位很方便,而鏈棧則要求每個元素都有指針域,無形中增加了內存開銷,但是鏈棧對長度是沒有限制的。

所以它們的區別和線性表的順序存儲以及鏈式存儲的區別是一樣的:如果棧中的元素個數變化較大,那麼使用鏈棧再合適不過,反之,如果元素個數變化較小,使用順序棧會更好一些。



作者:Xiao_Mai
鏈接:https://www.jianshu.com/p/66da0b8935ac
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章