棧(上)之順序棧

一、棧的定義
定義:棧(stack):棧是限定僅在表的一端進行插入或刪除操作的線性表。
我們把允許插入和刪除操作的一端稱爲棧頂(top),另一端稱爲棧底(bottom)。不含任何數據元素的棧稱爲空棧。棧又稱爲“後進先出(Last In First Out,簡稱LIFO)的線性表”,簡稱爲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
思考:能否得到結果爲312的出棧序列?
答案:不可能,若3先出棧,則意味着1和2已入棧,此時2的出棧一定在1之前。
練習:已知一個棧的入棧順序是a,b,c,d,e則不可能得到的出棧順序是:
A、abcde
B、edcba
C、dceab
D、decba

二、棧的順序存儲結構及實現
1、順序棧的結構定義
typedef int data_t;
typedef struct
{
data_t data[MAXSIZE];
int top;//棧頂元素位置
}SqStack;
當top=-1時,該棧爲空棧。當top=MAXSIZE-1時,該棧爲滿棧。
2、進棧操作Push
//代碼見附錄
3、出棧操作Pop
//代碼見附錄
對於進棧和出棧操作來說,二者都沒有用到循環語句,因此時間複雜度爲O(1)。
4、清空棧操作
清空一個棧的操作可以用以下方法實現:不斷彈棧,直至棧內沒有元素爲止。
但是這樣做實際上是沒有必要的。若要清空一個棧,則意味着該棧內的元素已經“無用”了,這時不用再每個元素進行彈棧操作,而直接將棧頂的位置拉下至棧底即可,即:
top=-1;
這樣,當新的元素再次壓棧時,會覆蓋掉原始的“無用”數據。
//代碼見附錄
三、棧的鏈式存儲結構及實現
對於順序棧來說,主要的缺點就是棧的大小已經固定,若有超過棧長的元素個數,則此時棧會發生“溢出”。這時我們可以採用鏈式棧的存儲結構,這樣就不用再考慮棧的空間是否足夠大的問題。
1、鏈棧的結構定義
棧的鏈式存儲結構,簡稱爲鏈棧。
思考:對於棧的鏈式存儲結構來說,棧頂指針是在鏈表頭結點位置更好,還是在鏈表尾節點位置更好?
答:頭結點位置更好
鏈表有頭指針,而棧的主要操作也是在棧頂進行,那麼我們就可以將二者合一,將單鏈表的頭指針作爲棧頂指針,即棧的鏈式存儲結構的棧頂指針爲單鏈表的頭指針。

typedef struct StackNode
{
data_t data;
struct StackNode *next;
}LinkStack;
2、判定鏈式棧是否爲空
對於鏈棧來說,基本不存在棧滿的情況,除非內存中已沒有可用空間。因此不考慮判定鏈式棧滿的操作。
對於鏈棧來說,棧空的情況實際上就是判斷top==NULL的情況。
//代碼見附錄
3、進棧操作Push
//代碼見附錄
4、出棧操作Pop
//代碼見附錄
對於進棧和出棧操作來說,二者都沒有用到循環語句,因此時間複雜度爲O(1)。
5、清空棧操作
//代碼見附錄
對比順序棧與鏈棧,我們發現它們在時間複雜度上是一樣的,均爲O(1)。對於空間性能,順序棧需要事先確定長度,可能會存在內存空間浪費問題。但它的優勢是存取時定位方便。而且鏈棧要求每個元素都有指針域,這同時也增加了內存開銷,但對於棧的長度無限制。
綜上所述,如果棧的使用過程中元素變化不可預料,有時數據量少有時卻很大,則我們推薦使用鏈棧。反之,如果數據變化在可控範圍內,則我們推薦使用順序棧。
四、棧的應用
1、遞歸
遞歸:函數在自身的函數體內直接或間接地調用自身。
示例:遞歸法求斐波那契數列
int Fbi(int i)
{
if(i<2)
return i==0?0:1;
return Fbi(i-1)+Fbi(i-2);
}
int main()
{
int i;
for(i=0;i<40;i++)
printf("%d\t",Fbi(i));
printf("\n");
return 0;
}
要實現遞歸,必要的兩個條件是遞歸出口和遞歸邏輯。在示例程序中,if(i<2)就是遞歸出口,而Fbi(i-1)+Fbi(i-2)就是遞歸邏輯。
//斐波那契數列的非遞歸解法略
對比遞歸代碼和非遞歸(迭代)代碼,我們可以看出遞歸和迭代的區別:迭代使用循環結構,而遞歸使用分支結構。
在某些程序中,遞歸能使得程序結構簡潔清晰,容易理解。但是大量的調用遞歸函數會建立許多該函數的副本,需要大量的內存存儲空間。而迭代法則無需大量的存儲空間。
要想實現遞歸,我們需要明白遞歸的過程本質上是函數返回順序是其調用順序的逆序,即:先行調用的函數會在後面獲得返回值。這種先行存儲數據,並在之後逆序恢復得到數據的過程,顯然很符合棧這種數據結構。因此,編譯器使用棧來實現函數的遞歸。
在調用階段,對於每層遞歸,函數的局部變量、參數、返回地址都被壓入棧中,再去調用下次遞歸。在返回階段,依次彈出位於棧頂的函數,獲得計算結果。這也是爲什麼需要“遞歸出口”的原因,遞歸出口可以看做是從壓棧到彈棧的狀態轉變因素。
2、後綴(逆波蘭)表示法
對於數學運算來說,確定運算符的優先級是十分重要的,直接決定了該算式是否計算正確。在實際生活中,我們書寫的算式都是中綴表達式,即運算符(此處特指算數運算符)在操作數中間。例如:
9+(3-1)*3+10/2
我們把這種平時使用的四則運算表達式的寫法稱爲中綴表達式。但是對於計算機而言,中綴表達式並不方便。計算機計算都是從左到右順序計算,在該算式中,*在+之後,但是卻要先於+進行運算,而加入括號後,運算則會變得更加複雜。
對於四則運算,20世紀50年代,波蘭邏輯學家Jan Lukasiewicz發明了一種不需要括號的表達式方法,稱爲後綴表示法,也稱爲逆波蘭(Reverse Polish Notation,簡稱RPN)表示法。
對於上文的算式,使用後綴表示法爲:
9 3 1 - 3 * + 10 2 / +
即運算符在兩個操作數之後出現。
那麼對於後綴表達法來說,計算機是怎樣計算的呢?
後綴表達式的算法規則:從左到右遍歷表達式,若遇到數字則進棧,遇到運算符則彈出棧頂兩個元素進行運算,計算結果再次壓棧,最後計算得到的結果就是最終結果。
我們以9 3 1 - 3 * + 10 2 / +進行講解
⒈初始化一個空棧,此棧用於對要計算的操作數的進出及存儲。
⒉9、3、1都是數字,因此依次入棧
⒊接下來是-,是符號,彈出棧頂兩個元素作爲操作數,注意先彈出的元素在符號右側,後彈出的元素在符號左側,即3 - 1,得到計算結果2,將2壓棧。
⒋數字3進棧
⒌後面是*,棧頂兩個元素彈棧進行運算 2 * 3,得到結果6,再壓入棧
⒍後面是+,棧頂兩個元素彈棧進行運算 9 + 6,得到結果15,再壓入棧
⒎數字10和2進棧
⒏後面是/,棧頂兩個元素彈棧進行運算 10 / 2,得到結果5,再壓入棧
⒐最後一個符號是+,棧頂兩個元素彈棧進行運算 15 + 5,得到結果20
⒑最終結果是20,棧變爲空,結束運算。
那麼,如何把中綴表達式轉化爲後綴表達式呢?
中綴表達式轉化爲後綴表達式的規則:從左到右遍歷中綴表達式的每個數字和符號,若是數字就輸出,即成爲後綴表達式的一部分;若是符號則判斷其與棧頂符號的優先級,是右括號或優先級低於或等於棧頂符號的則棧頂元素依次出棧並輸出,直至遇到一個比其優先級低的運算符爲止,並將當前符號進棧,一直到最終輸出後綴表達式爲止。
我們以9+(3-1)*3+10/2------>9 3 1 - 3 * + 10 2 / +爲例進行講解
1.初始化一個空棧,用於對符號進出棧使用。
2.第一個數字是9,輸出9。後面的符號+入棧。
3.第三個字符是(,依然是符號,因其是左括號還未配對,故進棧。
4.第四個字符是數字3,輸出,此時表達式爲9 3,接着符號-進棧。
5.接下來是數字1,輸出,此時表達式爲9 3 1,後面是符號),此時我們需要把(之前的所有元素都出棧,直至輸出(爲止。此時總的表達式是9 3 1 -。
6.緊接着是符號*,因爲此時的棧頂符號是+,優先級低於*,因此不輸出,*進棧。緊接着是數字3,輸出,總表達式爲9 3 1 – 3.
7.之後是符號+,此時棧頂元素是*,比+優先級高,因此棧中元素出棧並輸出(因爲沒有比+更低優先級的符號,所以全部出棧),總輸出表達式爲9 3 1 – 3 * +。然後將這個符號+進棧。
8.緊接着輸出數字10,總表達式爲9 3 1 – 3 * + 10。之後是符號/,所以/進棧。
9.最後一個數字爲2,此時總表達式爲9 3 1 – 3 * + 10 2。
10.因已到最後,所以將棧中符號全部出棧。最終獲得的後綴表達式爲9 3 1 – 3 * + 10 2 / +。
所以,對於計算機來說,輸入中綴表達式,獲得計算結果,最重要的有兩步:
⒈將中綴表達式轉化爲後綴表達式
⒉計算後綴表達式得到計算結果
而這兩步的整個過程都使用到了棧這種數據結構。

順序棧代碼
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 20
#define OK 1
#define ERROR 0

typedef int data_t;
typedef struct
{
data_t data[MAXSIZE];
int top;//棧頂元素所在的數組下標位置
}SqStack;

int PushStack(SqStack *s,data_t e)//壓棧
{
if(s->top==MAXSIZE-1)
{
printf("Stack is Full\n");
return ERROR;
}
s->top++;
s->data[s->top]=e;
return OK;
}

int PopStack(SqStack *s,data_t *e)//彈棧
{
if(s->top==-1)
{
printf("Stack is Empty\n");
return ERROR;
}
*e=s->data[s->top];
s->top--;
return OK;
}
SqStack* CreateEmptyStack()//創建棧
{
SqStack *stack = (SqStack*)malloc(sizeof(SqStack));
if(stack==NULL)
{
printf("CreateEmptyStack Error\n");
exit(0);
}
stack->top=-1;
return stack;
}
int EmptyStack(SqStack *s)//判斷棧是否是空棧
{
return -1==s->top?OK:ERROR;
}
int FullStack(SqStack *s)//判斷棧是否是滿棧
{
return MAXSIZE-1==s->top?OK:ERROR;
}
int ClearStack(SqStack *s)//清空棧內元素
{
s->top=-1;
return OK;
}
int main()
{
/*
SqStack *stack = CreateEmptyStack();
data_t data;
PushStack(stack,20);
PushStack(stack,30);
PopStack(stack,&data);
printf("pop:%d\n",data);
PopStack(stack,&data);
printf("pop:%d\n",data);
PopStack(stack,&data);
printf("pop:%d\n",data);
if(EmptyStack(stack)==OK)
printf("Stack is Empty!\n");
if(FullStack(stack)==OK)
printf("Stack is Full!\n");
*/
return 0;
}

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