【數據結構】用詳細圖文把「棧」搞明白(原理篇)


【系列文章合集】

前面已經介紹過了兩種線性表——順序存儲結構的順序表鏈式存儲結構的鏈表,也介紹了如何對其進行基本增刪改查操作。這兩種線性表的增加和刪除可以在表的任意位置進行操作,比如鏈表的頭插法和尾插法。

下面介紹一種特殊的線性表——棧。

1. 什么是棧?

棧,我們在日常生活中經常會聽到一個與之相關的詞語——棧道。什么是棧道?指沿懸崖峭壁修建的一種道路。李白詩《蜀道難》中的“天梯石棧相勾連”就是指這種棧:

圖片來自網絡

這種棧道的特點是很窄、很險,上圖的棧僅能容納一人,只能通過一側進來和出去。在上圖中,如果最左邊的人想要出去,就得等右邊的人全走了,他才能走。也即,最先進來的最后出去,最后進來的最先出去。

棧的英文是 Stack,本義是“一堆成疊的”。比如一堆成疊的書:

在這疊書中,放書和拿書都從上面,要想拿到底下那個最大的,就得先把上面的幾個小的先拿掉。也即,先放的書最后才能拿,最后放的書可以最先那。

有了這兩個實際的例子,我們心中對“棧”這個數據結構就有了一個“形狀”了。

首先,棧是一個線性表(線性表的詳細介紹),所以它得具有以下特點:

  • 線性表由若干元素組成,用來存儲信息。
  • 元素之間有順序。
  • 除了首元素(只有一個直接后繼元素)和尾元素(只有一個直接前驅元素)外,其它元素都有且僅有一個直接前驅元素和一個直接后繼元素。

其次,棧是一個受限的線性表,受限之處為:

  • 只能在一端進行操作(增刪改查等)

根據以上總結的特點,我們可以畫出棧的示意圖,由於只能在一端進行操作,所以我們可以將其畫為只有一個開口的“容器”:

棧的示意圖

進行插入和刪除操作的那一端稱為棧頂(表尾),另一端稱為棧底(表頭)。

棧有兩種重要的操作——入棧(壓棧)和出棧(彈棧)。

所謂入棧(壓棧),即棧的插入操作,由於棧的只能從棧頂插入元素的特性,所以插入元素看起來是將元素給“壓入”棧。

入棧示意圖

所謂出棧(彈棧),即棧的刪除操作,由於棧的只能從棧頂刪除元素的特性,所以刪除元素看起來是將元素給“彈出”棧,彈出的元素必定是棧頂元素。

出棧示意圖

棧的只能在一端操作的特性,導致棧具有一個非常特殊的特點,下圖中的棧,元素入棧的順序為:1、2、3、4,但是元素出棧的順序則為:4、3、2、1。

也即,先入棧的后出棧(First In Last Out, FILO),后入棧的先出棧(Last In First Out, LIFO),這是棧作為一種受限的線性表的非常重要的特性。

總結一下:棧是一種只能在表尾操作的后入先出的受限的線性表。

2. 棧的實現思路

棧雖然是一種受限的線性表,但線性表有的一些基本特性,棧也具備。在前面已經介紹過了線性表的順序存儲結構(數組實現)和線性表的鏈式存儲結構(鏈表實現),棧也可以使用這兩種方式來實現得到數組棧和鏈表棧。

2.1. 數組實現——數組棧

分析一下棧的結構就可以知道棧有兩個必要結構:

  • 用來存儲數據的數組—— data[]
  • 用來表示線性表的最大存儲容量的值——MAXSIZE
  • 用來標識棧頂元素的棧頂下標—— top

這里規定:棧頂下標是棧頂元素的下標。

棧頂下標還可以表示棧的當前長度。

棧——數組實現

使用 C 語言的結構體實現如下:

為了方便起見,這里的棧只存儲整數

#define MAXSIZE 5 //棧的最大存儲容量

/*數組棧的結構體*/
typedef struct {
    int data[MAXSIZE]; //存儲數據的數組
    int top; //棧頂下標
}

2.2. 鏈表實現——鏈表棧

首先我們得先了解單鏈表的具體原理及實現,詳細介紹移步至文章【單鏈表】

鏈表棧的結構和數組棧的結構有所不同,其必要結構如下:

  • 鏈表的基本單元結點 —— StackNode
    • 結點的數據域—— data
    • 結點的指針域—— next
  • 指向鏈表頭的頭指針 —— head
  • 指向棧頂結點的棧頂指針 —— top

為了方便起見,我們可以再添加一個棧的長度—— length

前面說了,棧是一種只能在表尾操作的后入先出的受限的線性表。放在鏈表中,就是只在鏈表尾或鏈表頭操作。那么是選擇鏈表尾還是鏈表頭呢?

上面已經列出了鏈表棧的必要結構,其中包括了兩個指針:頭指針和棧頂指針。我們可以把這兩個指針合二為一,即使用鏈表的頭指針作為棧的棧頂指針,如此一來,鏈表棧的操作就需要放在鏈表頭進行,即借用鏈表頭插法和頭刪法完成棧的 pushpop

棧——鏈表實現

數組棧的容量是固定的,而鏈表棧的容量則不是固定的。在這里,我們使用不帶頭結點的鏈表來實現棧。

代碼實現如下:

/*鏈表棧結點的結構體*/
typedef struct StackNode {
    int data; //數據域
    struct StackNode *next; //指針域
} StackNode;
/*棧的結構體*/
typedef struct StackLink {
    StackNode *top; //棧頂指針
    int length; //棧的長度
} StackLink;

3. 棧的狀態

3.1. 數組棧的狀態

數組棧有三種狀態:空棧、滿棧、非空非滿棧。通過棧頂下標棧的最大容量之間的關系,可以很容易判斷出這三種狀態。

【空棧】:棧中沒有元素。

因為數組下標是從 0 開始的,所以此時棧頂下標 top 的值通常置為 -1,以此表示棧中無元素。

空棧

【滿棧】:棧中元素已滿,沒有多余容量。

滿棧

從圖中可以看出,棧滿時滿足條件 top = MAXSIZE - 1

【非空非滿棧】: 棧不是空棧且容量仍有剩余。

非空非滿棧

此時的棧滿足條件 -1 < top < MAXSIZE - 1

3.2. 鏈表棧的狀態

數組棧之所以有三種狀態,是因為有最大容量這個限制,而鏈表棧的元素不收約束,所以鏈表棧只有空棧和非空棧兩種狀態。

當為空棧時,棧頂指針和頭指針都指向 NULL:

空棧

4. 初始化

所謂初始化,即把棧初始化為空棧的狀態。

4.1. 數組棧的初始化

將數組棧的棧頂下標置為 -1 即可。

/**
 * 數組棧的初始化:將棧的棧頂下標置為 -1
 * stack: 指向要操作的棧的指針
 */
void init(StackArray *stack)
{
    stack->top = -1;
}

4.2. 鏈表棧的初始化

需要將棧頂指針 top (即鏈表頭指針 head)置為 NULL,將棧的長度 length 置為 0

/**
 * 初始化:將棧頂指針置為 NULL,長度置為 0
 * stack: 指向要操作的棧的指針
 */
void init(StackLink *stack)
{
    stack->top = NULL;
    stack->length = 0;
}

5. 入棧操作

5.1. 數組棧

入棧前我們要搞清楚一個問題:

由於棧頂下標是棧頂元素的下標,所以在入棧前我們需要先將棧頂下標“上移”,給新入棧的元素騰出位置。然后才能將新元素入棧。

數組棧入棧

在入棧前先檢查一下棧是否已滿,具體代碼實現如下:

/**
 * 入棧操作
 * stack: 指向要操作的棧的指針
 * elem: 要入棧的數據
 * return: 0失敗,1成功
 */
int push(StackArray *stack, int elem)
{
    if (stack->top == MAXSIZE - 1) {
        printf("棧已滿,無法繼續入棧。\n");
        return 0;
    }
    stack->top++;
    stack->data[stack->top] = elem;
    return 1;
}

5.2. 鏈表棧

鏈表棧的入棧操作實質為[頭插法](###2.7.2. 頭插法),過程如下:

鏈表棧入棧

具體代碼實現如下:

StackNode *create_node(int elem)
{
    StackNode *new = (StackNode *) malloc(sizeof(StackNode));
    new->data = elem;
    new->next = NULL;
    return new;
}

/**
 * 入棧操作: 本質是單鏈表的尾插法
 * head: 頭指針
 * elem: 要入棧的結點的值
 */
void push(StackLink *stack, int elem)
{
    StackNode *new = create_node(elem);
    // 鏈表的頭插法
    new->next = stack->top;
    stack->top = new;
    //棧長度加一
    stack->length++;
}

6. 出棧操作

6.1. 數組棧

出棧操作和入棧操作剛還相反,即先將元素出棧,然后將棧頂下標“下移”。

數組棧出棧

出棧前先檢查棧是否為空棧,具體代碼實現如下:

/**
 * 出棧操作
 * stack: 指向要操作的棧的指針
 * elem: 指向保存變量的指針
 * return: 0失敗,1成功
 */
int pop(StackArray *stack, int *elem)
{
    if (stack->top == -1) {
        printf("棧空,無元素可出棧。\n");
        return 0;
    }
    *elem = stack->data[stack->top];
    stack->top--;
    return 1;
}

6.2. 鏈表棧

鏈表棧的出棧操作實質為頭刪法,即從鏈表頭刪除結點,過程如下:

鏈表棧出棧

出棧前先檢查棧是否為空棧,具體代碼實現如下:

/**
 * 出棧操作
 * stack: 指向要操作的棧的指針
 * elem: 指向保存變量的指針
 * return: 0失敗,1成功
 */
int pop(StackLink *stack, int *elem)
{
    if (stack->length == 0) {
        printf("棧空,無元素可出棧。\n");
        return 0;
    }
    // top_node 指向棧頂結點
    StackNode *top_node = stack->top;
    //保存棧頂結點的值
    *elem = top_node->data;
    //棧頂指針下移
    stack->top = top_node->next;
    //釋放 top_node
    free(top_node);
    stack->length--;
    return 1;
}

7. 遍歷棧

這里以打印棧為例來介紹如何遍歷棧。

7.1. 數組棧

數組棧的遍歷本質是在遍歷數組,一個 for 循環即可搞定。

/**
 * 打印棧
 * stack: 要打印的棧
 */
void output(StackArray stack)
{
    if (stack.top == -1) {
        printf("空棧。\n");
        return;
    }
    for (int i = stack.top; i >= 0; i--) {
        printf("%d ", stack.data[i]);
    }
    printf("\n");
}

7.2. 鏈表棧

鏈表棧的遍歷本質是在遍歷鏈表,借助一個輔助指針從棧頂開始進行 whilefor 循環即可。

/**
 * 打印棧
 * stack: 要打印的棧
 */
void output(StackLink *stack)
{
    if (stack->length == 0) {
        printf("空棧。\n");
        return;
    }
    StackNode *p = stack->top;
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

完整代碼請移步至 GitHub | Gitee 獲取。

以上就是棧的基本原理介紹。

如有錯誤,還請指正。

如果感覺寫的不錯,可以點個關注。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM