棧
棧的定義
棧是限制在表的一端進行插入和刪除的線性表。允許插入、刪除的這一端稱為棧頂,另 一個固定端稱為棧底。當表中沒有元素時稱為空棧。
棧頂:通常將表中允許進行插入、刪除操作的一端稱為棧頂 (Top),因此棧頂的當前位 置是動態變化的,它由一個稱為棧頂指針的位置指示器指示。
棧底:同時表的另一端被稱為棧底 (Bottom)。當棧中沒有元素時稱為空棧。棧的插入 操作被形象地稱為進棧或入棧,刪除操作稱為出棧或退棧。
棧是運算受限的線性表,線性表的存儲結構對棧也是適用的,只是操作不同而已。
利用順序存儲方式實現的棧稱為順序棧。
(1) InitStack(S)
操作前提:S 為未初始化的棧。 操作結果:將 S 初始化為空棧。
(2) ClearStack(S)
操作前提:棧 S 已經存在。 操作結果:將棧 S 置成空棧。
(3) IsEmpty(S)
操作前提:棧 S 已經存在。 操作結果:判棧空函數,若 S 為空棧則函數值為“TRUE”,否則為“FALSE”。
(4) IsFull(S)
操作前提:棧 S 已 經存在。 操作結果:判棧滿函數,若 S 棧已滿,則函數值為“TRUE”,否則為“FALSE”。
(5) Push(S,x)
操作前提:棧 S 已經存在。 操作結果:在 S 的頂部插入(亦稱壓入)元素 x;若 S 棧未滿,將 x 插入棧頂 位置,若棧已滿,則返回 FALSE,表示操作失敗,否則返回 TRUE。
(6) Pop(S, x)
操作前提:棧 S 已經存在。 操作結果:刪除(亦稱彈出)棧 S 的頂部元素,並用 x 帶回該值; 若棧為空,返回值為 FALSE,表示操作失敗,否則返回 TRUE。
(7) GetTop(S, x)
操作前提:棧 S 已經存在。 操作結果:取棧 S 的頂部元素賦給 x 所指向的單元。與 Pop(S, x)不同之處在 於 GetTop(S,x)不改變棧頂的位置。
與線性表類似,棧的動態分配順序存儲結構如 下:
#define STACK_INIT_SIZE 100 //存儲空間的初始分配量 #define STACKINCREMENT 10 //存儲空間的分配增量 typedef struct{ SElemType *base; //在棧構造之前和銷毀之后,base 的值為 NULL SElemType *top; //棧頂指針 int stacksize; //當前已分配的存儲空間 }SqStack;
需要注意,在棧的動態分配順序存儲結構中,base 始終指向棧底元素,非空棧中的 top 始終在棧頂元素的下一個位置。
下面是順序棧上常用的基本操作的實現。
(1)入棧:若棧不滿,則將 e 插入棧頂。
int Push (SqStack &S, SElemType e) { if (S.top-S.base>=S.stacksize) {……} //棧滿,追加存儲空間 *S.top++ = e; //top始終在棧頂元素的下一個位置 return OK; }
(2)出棧:若棧不空,則刪除 S 的棧頂元素,用 e 返回其值,並返回 OK,否則返回 ERROR。
int Pop (SqStack &S, SElemType &e) { if (S.top==S.base) return ERROR; e = *--S.top; return OK; }
出棧和讀棧頂元素操作,先判棧是否為空,為空時不能操作,否則產生錯誤。通常棧空常作為一種控制轉移的條件。
順序棧的兩棧共享
棧的應用非常廣泛,經常會出現在一個程序中需要同時使用多個棧的情況。若使用順序 棧,會因為對棧空間大小難以准確估計,從而產生有的棧溢出、有的棧空間還很空閑的情況。
多棧共享技術:
可以讓多個棧共享一個足夠大的數組空間,通過利用棧的動態特性來使 其存儲空間互相補充,這就是多棧共享技術。
在順序棧的共享技術中最常用的是兩個棧的共享技術即雙端棧:它主要利用了棧“棧底 位置不變,而棧頂位置動態變化”的特性。首先為兩個棧申請一個共享的一維數組空間 S[M], 將兩個棧的棧底分別放在一維數組的兩端,分別是 0,M-1。由於兩個棧頂動態變化,這樣 可以形成互補,使得每個棧可用的最大空間與實際使用的需求有關。由此可見,兩棧共享要 比兩個棧分別申請 M/2 的空間利用率要高。兩棧共享的數據結構定義如下:
#define M 100 typedef struct { StackElementType Stack[M]; StackElementType top[2]; /*top[0]和 top[1]分別為兩個棧頂指示器*/ }DqStack;
兩個棧共用時的初始化、進棧和出棧操作 的算法:
⑴ 初始化操作。
【算法描述】
void InitStack(DqStack *S) { S->top[0]=-1; S->top[1]=M; }
⑵ 進棧操作。
【算法描述】
int Push(DqStack *S, StackElementType x, int i) {/*把數據元素 x 壓入 i 號堆棧*/ if(S->top[0]+1==S->top[1]) /*棧已滿*/ return(FALSE); switch(i) { case 0: S->top[0]++; S->Stack[S->top[0]]=x; break; case 1: S->top[1]--; S->Stack[S->top[1]]=x; break; default: /*參數錯誤*/ return(FALSE) } return(TRUE); }
⑶ 出棧操作。
【算法描述】
int Pop(DqStack *S, StackElementType *x, int i) {/* 從 i 號堆棧中彈出棧頂元素並送到 x 中 */ switch(i) { case 0: if(S->top[0]==-1) return(FALSE); *x=S->Stack[S->top[0]]; S->top[0]--; break; case 1: if(S->top[1]==M) return(FALSE); *x=S->Stack[S->top[1]]; S->top[1]++; break; default: return(FALSE); } return(TRUE); }
棧的鏈式實現
鏈棧即采用鏈表作為存儲結構實現的棧。
為便於操作,這里采用帶頭結點的單鏈表實現棧。由於棧的插入和刪除操作僅限制在表頭位置進行,所以鏈表的表頭指針就作為棧頂指 針,如下圖所示。
棧鏈示意圖:
在上圖中,top 為棧頂指針,始終指向當前棧頂元素前面的頭結點。若 top->next=NULL, 則代表棧空。采用鏈棧不必預先估計棧的最大容量,只要系統有可用空間,鏈棧就不會出現溢出。采用鏈棧時,棧的各種基本操作的實現與單鏈表的操作類似,對於鏈棧,在使用完畢 時,應該釋放其空間。
鏈棧的結構可用 C 語言定義如下:
typedef struct node { StackElementType data; struct node *next; }LinkStackNode; typedef LinkStackNode *LinkStack;
進棧、出棧等最主要的運算 實現。
⑴ 進棧操作
【算法描述】
int Push(LinkStack top, StackElementType x) /* 將數據元素 x 壓入棧 top 中 */ { LinkStackNode * temp; temp=(LinkStackNode * )malloc(sizeof(LinkStackNode)); if(temp==NULL) return(FALSE); /* 申請空間失敗 */ temp->data=x; temp->next=top->next; top->next=temp; /* 修改當前棧頂指針 */ return(TRUE); }
⑵ 出棧操作
【算法描述】
int Pop(LinkStack top, StackElementType *x) { /* 將棧 top 的棧頂元素彈出,放到 x 所指的存儲空間中 */ LinkStackNode * temp; temp=top->next; if(temp==NULL) /*棧為空*/ return(FALSE); top->next=temp->next; *x=temp->data; free(temp); /* 釋放存儲空間 */ return(TRUE); }
int Pop(LinkStack top, StackElementType *x) { /* 將棧 top 的棧頂元素彈出,放到 x 所指的存儲空間中 */ LinkStackNode * temp; temp=top->next; if(temp==NULL) /*棧為空*/ return(FALSE); top->next=temp->next; *x=temp->data; free(temp); /* 釋放存儲空間 */ return(TRUE); }
int Pop(LinkStack top, StackElementType *x) { /* 將棧 top 的棧頂元素彈出,放到 x 所指的存儲空間中 */ LinkStackNode * temp; temp=top->next; if(temp==NULL) /*棧為空*/ return(FALSE); top->next=temp->next; *x=temp->data; free(temp); /* 釋放存儲空間 */ return(TRUE); }
棧的應用舉例
由於棧的“先進先出”特點,在很多實際問題中都利用棧做一個輔助的數據結構來進行求解。
1、數值轉換
2、表達式求值
表達式求值是程序設計語言編譯中一個基本的問題,它的實現也是需要棧的加入。下 面的算法是由運算符優先法對表達式求值。在此僅限於討論只含二目運算符的算術表達式。
(1)中綴表達式求值
中綴表達式:每個二目運算符在兩個運算量的中間,假設所討論的算術運算符包括:+ 、 - 、、/、%、^(乘方)和括號()。
設運算規則為:
.運算符的優先級為:()——> ^ ——>*、/、%——> +、- ;
.有括號出現時先算括號內的,后算括號外的,多層括號,由內向外進行;
.乘方連續出現時先算右面的。
表達式作為一個滿足表達式語法規則的串存儲,如表達式“3*2^(4+2*2-1*3)-5”,它的求值過程為:自左向右掃描表達式,當掃描到 3*2 時不能馬上計算,因為后面可能還有更高的運算,正確的處理過程是:需要兩個棧:對象棧 s1 和運算符棧 s2。當自左至右掃描表達式的每一個字符時,若當前字符是運算對象,入對象棧,是運算符時,若這個運算符比棧頂運算符高則入棧,繼續向后處理,若這個運算符比棧頂運算符低則從對象棧出棧兩個運算量, 從運算符棧出棧一個運算符進行運算,並將其運算結果入對象棧,繼續處理當前字符,直到遇到結束符。
為了處理方便,編譯程序常把中綴表達式首先轉換成等價的后綴表達式,后綴表達式的運算符在運算對象之后。在后綴表達式中,不在引入括號,所有的計算按運算符出現的順序, 嚴格從左向右進行,而不用再考慮運算規則和級別。中綴表達式“3*2^(4+2*2-1*3)-5 ”的后 綴表達式為:“32422*+13-^*5-”
(2) 后綴表達式求值
計算一個后綴表達式,算法上比計算一個中綴表達式簡單的多。這是因為表達式中即無括號又無優先級的約束。具體做法:只使用一個對象棧,當從左向右掃描表達式時,每遇到一個操作數就送入棧中保存,每遇到一個運算符就從棧中取出兩個操作數進行當前的計算, 然后把結果再入棧,直到整個表達式結束,這時送入棧頂的值就是結果。
下面是后綴表達式求值的算法,在下面的算法中假設,每個表達式是合乎語法的,並且 假設后綴表達式已被存入一個足夠大的字符數組 A 中,且以‘#’為結束字符,為了簡化問題, 限定運算數的位數僅為一位且忽略了數字字符串與相對應的數據之間的轉換的問題。
typedef char SElemType ; double calcul_exp(char *A){ //本函數返回由后綴表達式 A 表示的表達式運算結果 SqStack s ; ch=*A++ ; InitStack(s) ; while ( ch != ’#’ ){ if (ch!=運算符) Push (s , ch) ; else { Pop (s , &a) ; Pop (s , &b) ; //取出兩個運算量 switch (ch).{ case ch= =’+’: c=a+b ; break ; case ch= =’-’: c=a-b ; break ; case ch= =’*’: c=a*b ; break ; case ch= =’/’: c=a/b ; break ; case ch= =’%’: c=a%b ; break ; } Push (s, c) ; } ch=*A++ ; } Pop ( s , result ) ; return result ; }
(3) 中綴表達式轉換成后綴表達式:
將中綴表達式轉化為后綴表達示和前述對中綴表達式求值的方法完全類似,但只需要運 算符棧,遇到運算對象時直接放后綴表達式的存儲區,假設中綴表達式本身合法且在字符數組 A 中,轉換后的后綴表達式存儲在字符數組 B 中。
具體做法:遇到運算對象順序向存儲后綴表達式的 B數組中存放,遇到運算符時類似於中綴表達式求值時對運算符的處理過程,但運算符出棧后不是進行相應的運算,而是將其送入B 中存放。
3、棧與遞歸
在高級語言編制的程序中,調用函數與被調用函數之間的鏈接和信息交換必須通過棧進行。當在一個函數的運行期間調用另一個函數時,在運行該被調用函數之前,需先完成三件事:
(1)將所有的實在參數、返回地址等信息傳遞給被調用函數保存;
(2)為被調用函數的局部變量分配存儲區;
(3)將控制轉移到被調用函數的入口。
從被調用函數返回調用函數之前,應該完成:
(1)保存被調函數的計算結果;
(2)釋放被調函數的數據區;
(3)依照被調函數保存的返回地址將控制轉移到調用函數。
多個函數嵌套調用的規則是:后調用先返回,此時的內存管理實行“棧式管理”。
遞歸函數的調用類似於多層函數的嵌套調用,只是調用單位和被調用單位是同一個函數而已。
將遞歸程序轉化為非遞歸程序時常使用棧來實現。
遞歸與非遞歸轉換
1. 遞歸算法到非遞歸算法的轉換
遞歸算法具有兩個特性:
①遞歸算法是一種分而治之、把復雜問題分解為簡單問題的求解問題方法, 對求解某些復雜問題,遞歸算法的分析方法是有效的。
②遞歸算法的效率較低。 為此,在求解某些問題時,希望用遞歸算法分析問題,用非遞歸算法求解具體問 題。
(1)消除遞歸的原因:
其一,有利於提高算法時空性能,因為遞歸執行時需要系統提供隱式棧實現 遞歸,效率較低。
其二,無應用遞歸語句的語言設施環境條件,有些計算機語言不支持遞歸功 能,如 FORTRAN 語言中無遞歸機制 。
其三,遞歸算法是一次執行完,中間過程對用戶不可見,這在處理有些問題 時不合適,也存在一個把遞歸算法轉化為非遞歸算法的需求。
常用的兩類消除遞歸方法:
一類是簡單遞歸問題的轉換,對於尾遞歸和單向遞歸的算法,可用循環結構 的算法替代。
另一類是基於棧的方式,即將遞歸中隱含的棧機制轉化為由 用戶直接控制的 明顯的棧。利用堆棧保存參數,由於堆棧的后進先出特性吻合遞歸算法的執行過 程,因而可以用非遞歸算法替代遞歸算法。
(2) 簡單遞歸的消除
在簡單情況下,可以將遞歸算法轉化為線性操作序列,直接用循環實現。
①單向遞歸
單向遞歸是指遞歸函數中雖然有一處以上的遞歸調用語句,但各次遞歸調用語句 的參數只和主調用函數有關,相互之間參數無關,並且這些遞歸調用語句處於算法的最后。 計算斐波那契數列的遞歸算法 Fib(n) 是單向遞歸的一個典型例子。
②尾遞歸
尾遞歸是指遞歸調用語句只有一個,而且是處於算法的最后,尾遞歸是單向遞歸的特例。 以階乘問題的遞歸算法 Fact(n)為例討論尾遞歸算法的運行過程。