【系列文章合集】
前面已經介紹過了兩種線性表——順序存儲結構的順序表和鏈式存儲結構的鏈表,也介紹了如何對其進行基本增刪改查操作。這兩種線性表的增加和刪除可以在表的任意位置進行操作,比如鏈表的頭插法和尾插法。
下面介紹一種特殊的線性表——棧。
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。
前面說了,棧是一種只能在表尾操作的后入先出的受限的線性表。放在鏈表中,就是只在鏈表尾或鏈表頭操作。那么是選擇鏈表尾還是鏈表頭呢?
上面已經列出了鏈表棧的必要結構,其中包括了兩個指針:頭指針和棧頂指針。我們可以把這兩個指針合二為一,即使用鏈表的頭指針作為棧的棧頂指針,如此一來,鏈表棧的操作就需要放在鏈表頭進行,即借用鏈表頭插法和頭刪法完成棧的 push 和 pop。

數組棧的容量是固定的,而鏈表棧的容量則不是固定的。在這里,我們使用不帶頭結點的鏈表來實現棧。
代碼實現如下:
/*鏈表棧結點的結構體*/
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. 鏈表棧
鏈表棧的遍歷本質是在遍歷鏈表,借助一個輔助指針從棧頂開始進行 while 或 for 循環即可。
/**
* 打印棧
* 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");
}
以上就是棧的基本原理介紹。
如有錯誤,還請指正。
如果感覺寫的不錯,可以點個關注。

