1. 程式人生 > >棧---定義、應用(遞迴、字尾表示式實現數學表示式求值)

棧---定義、應用(遞迴、字尾表示式實現數學表示式求值)

一、定義

是限定僅在表尾進行插入和刪除操作的線性表。因此,棧的表尾端稱為棧頂;表頭端稱為棧底。不含任何資料元素的棧稱為空棧。棧又稱為後進先出(Last In First Out)的線性表,簡稱LIF0結構。

理解棧的定義需要注意:首先它是一個線性表,也即棧元素具有線性關係,即前驅後繼關係。只不過它是一種特殊的線性表而已。

棧的插入操作,叫作進棧,也稱壓棧、入棧。棧的刪除操作,叫作出找,也有的叫作彈棧。
這裡寫圖片描述

如線性表一樣,棧也有順序儲存與鏈式儲存。

二、應用

1、用瀏覽器上網時,不管什麼瀏覽器都有一個“後退”鍵,你點選後可以按訪問順序的逆序載入瀏覽過的網頁。即使你從一個網頁開始,連續點了幾十個連結跳轉,你點“後退” 時,還是可以像歷史倒退一樣,回到之前瀏覽過的某個頁面。

2、很多類似的軟體,比如Word、Photoshop等文件或影象編輯軟體中,都有撤銷(undo)的操作,也是用棧這種方式來實現的,當然不同的軟體具體實現程式碼會有很大差異,不過原理其實都是一樣的。

3、實現遞迴—斐波那契數列

斐波那契數列迭代版本:

#include "stdio.h"

int main()
{
    int i;
    int a[20];
    printf("迭代顯示斐波那契數列:\n");
    a[0]=0;
    a[1]=1;
    printf("%d ",a[0]);
    printf("%d ",a[1]);
    for(i = 2
;i < 20;i++) { a[i] = a[i-1] + a[i-2]; printf("%d ",a[i]); } printf("\n"); return 0; }

斐波那契數列遞迴版本:

#include "stdio.h"

int Fbi(int i)  // 斐波那契的遞迴函式 
{
    if( i < 2 )
        return i == 0 ? 0 : 1;
    return Fbi(i - 1) + Fbi(i - 2);  
}

int main()
{
    int i;

    printf
("遞迴顯示斐波那契數列:\n"); for(i = 0;i < 20;i++) printf("%d ", Fbi(i)); return 0; }

對比兩種實現斐波那契的程式碼。迭代和遞迴的區別是:迭代使用的是迴圈結構,遞迴使用的是選擇結構。

遞迴能使程式的結構更清晰、更簡潔、更容易讓人理解,從而減少讀懂程式碼的時間。但是大量的遞迴呼叫會建立函式的副本,會耗費大量的時間和記憶體
迭代則不需要反覆呼叫函式和佔用額外的記憶體。因此我們應該視不同 情況選擇不同的程式碼實現方式。

遞迴和棧有什麼關係呢?

前面我們已經看到遞迴是如何執行它的前行和退回階段的。遞迴過程退回的順序是它前行順序的
逆序。在退回過程中,可能要執行某些動作,包括恢復在前行過程中儲存起來的某些資料。

這種儲存某些資料,並在後面又以儲存的逆序恢復這些資料,以提供之後使用的需求,顯然很符合棧這樣的資料結構,因此,編譯器使用棧實現遞迴就沒什麼好驚訝的了。

簡單的說,就是在前行階段,對於每一層遞迴,函式的區域性變數、引數值以及返回地址都被壓入棧中。在退回階段,位於棧頂的區域性變數、引數值和返回地址被彈出,用於返回呼叫層次中執行程式碼的其餘部分,也就是恢復了呼叫的狀態。!!!!!

4、數學表示式的求值—字尾表示式

對於9+(3-1)×3+10/2而言,如何用計算機實現求值?

仔細觀察後發現,括號都是成對出現的,有左括號就一定會有右括號,對於多重括號,最終也是完全巢狀匹配的。這用棧結構正好合適,只有碰到左括號,就將此左括號進找,不管表示式有多少重括號,反正遇到左括號就進棧,而後面出現右括號時,就讓棧頂的左括號出棧,期間讓數字運算,這樣,最終有括號的表示式從左到右巡査一遍,棧應該是由空到有元素,最終再因全部匹配成功後成為空棧的結果。

但對於四則運算,括號也只是當中的一部分,先乘除後加減使得問題依然複雜, 如何有效地處理它們呢?波蘭邏輯學家Jan tukasiewicz想到了一種不需要括號的字尾表達法,我們也把它稱為逆波蘭(Reverse Polish Notation, RPN)表示。

對於9+(3-1)*3+10/2如果要用字尾表示法應該是什麼樣子:

9 3 1-3*+ 10 2/+

這樣的表示式稱為字尾表示式,叫字尾的原因在於所有的符號都是在要運算數字的後面出現。

規則:

從左到右遍歷表示式的每個數字和符號,遇到是數字就進棧,遇到是符號,就將處於棧頂兩個數
字出棧,進行運算,運算結果進棧,一直到最終獲得結果。

步驟:
(1)初始化一個空棧。此桟用來對要運算的數字進出使用。
(2)字尾表示式中前三個都是數字,所以9、3、1進棧。
這裡寫圖片描述

(3)接下來是減號“-”,所以將棧中的1出棧作為減數,3出棧作為被減數,並運算3-1得到2,再將2進棧。
(4)接著是數字3進棧。
這裡寫圖片描述

(5)後面是乘法“*”,也就意味著棧中3和2出棧,2與3相乘,得到6,並將6進棧。
(6) 下面是加法“+”,所以找中6和9出找,9與6相加,得到15,將15進棧。
這裡寫圖片描述

(7)接著是10與2兩數字進棧。
(8)接下來是除號“/”因此,棧頂的2與10出棧,10與2相除,得到5,將5進棧。
這裡寫圖片描述

(9)最後一個是符號“+”,所以15與5出棧並相加,得到20,將20進棧。
(10)結果是20出棧,棧變為空。
這裡寫圖片描述

綜上,我們實現了利用字尾表示式對數學表示式求值。

這個字尾表示式9 3 1-3*+ 10 2/+是如何通過算式9+(3-1)*3+10/2變化而來的呢?

我們把平時所用的標準四則運算表示式,即9+(3-1)*3+10/2叫做中綴表示式。因為所有的運算子號都在兩數字的中間,現在我們的問題就是中綴到字尾的轉化。

中綴表示式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 * +.然後將當前這個符號“+”進棧。也就是說,前6張圖的棧底的“+”是指中綴表示式中開頭的9後面那個“+”,而下圖中的棧底(也是棧頂)的“+”是指“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/+
這裡寫圖片描述

從剛才的推導中你會發現,要想讓計算機具有處理我們通常的標準(中綴)表示式的能力,最重要的就是兩步:

(1)將中綴表示式轉化為字尾表示式(棧用來進出運算的符號)。
(2)將字尾表示式進行運算得出結果(棧用來進出運算的數字)。

整個過程都充分利用了棧的後進先出特性來處理。