摘要
在這篇文章裡,我將介紹資料結構Stack的基本操作和它的一些應用。
我們將看到Stack在括弧匹配檢測,運算式求值,函數調用上的應用。
遞迴是一種特殊的函數調用,由於遞迴在程式設計中十分重要且不容易理解,所以我將闡述我對遞迴的理解。
最後我們將看到利用Stack和遞迴是怎麼優雅的解決一個經典遊戲:漢諾塔。
本文還將給出運算式求值和漢諾塔的HTML5示範。
Stack簡介
Stack即棧,以下是維基百科的定義:
在電腦科學中,是一種特殊的串列形式的資料結構,它的特殊之處在於只能允許在鏈結串列或陣列的一端稱為堆棧頂端指標,英語:top)進行加入資料英語:push)和輸出資料英語:pop)的運算。另外堆棧也可以用一維陣列或連結串列的形式來完成。
根據定義我們知道棧只有三種操作:入棧(push),出棧(pop),擷取棧頂元素(top)。而且棧只能夠操縱棧頂元素,即只能在一端進行操作。
由於棧具有後進入的元素率先彈出的性質,棧又被稱為後進先出(LIFO, Last In First Out)的結構。
棧的操作十分簡單,我們可以用單鏈表(LinkedList)和數組來實現棧。
然而在JavaScript中,Array內建pop(), push()的操作,而且我們可以利用Array[Array.length-1]來實現top()操作。所以沒有必要去另外實現一個Stack類型,用Array表達即可。
應用
Stack的LIFO的特性使得其適於解決許多實際問題,以下我們選取它的三個應用來加以闡述。
括弧匹配檢測
我們平時在編輯器中寫代碼時,有些編輯器會自動檢測括弧是否前後匹配,不匹配的話則會報錯提示。
利用Stack的LIFO的特性,我們可以輕鬆實現這個功能。
演算法的虛擬碼如下:
- //建立一個Stack s
- s = new stack()
- //讀取字元直至讀完
- while read to c != EOF:
- //如果字元是開放括弧 如 '(' '[' '{'等, 入棧
- if c is opening:
- s.push( c )
- //如果字元是結束括弧 如 ')' ']' '}'
- else if c is closing:
- //若棧為空白或者棧頂元素與開放括弧不匹配 則報錯
- if s is empty or f s.pop() is not correspond to c:
- return error!
- //若最後棧不為空白,報錯
- if s is not empty:
- return error!
- //如果沒有返回報錯,則返回正常
- return ok
演算法的原理為,遇到一個結束的括弧時,我們總是要尋找最後一個開放的括弧是否與之匹配,若找不到開放的括弧,或最後一個開放的括弧不匹配,則報錯。
由於總是而且僅需要尋找最後一個元素,所以我們將開放的括弧入棧,匹配時則出棧。
由於Stack的特性,這個演算法簡單明了,且消耗的時間複雜度為線性級O(n)。
運算式求值
Stack的強大特性,也使得其能夠運用在運算式求值上。
設想一個運算式:2+4/(3-1)
這個運算式具備了三種類型的符號:
計算它的演算法如下:
- //分配兩個棧,ops為運算子棧,nums為數字棧
- ops = new Stack, nums = new Stack
- //從運算式中讀取字元 直至結束
- while read c in expression != EOF:
- //若為左括弧,入運算子棧
- if c is '(':
- ops.push(c)
- //若為數字,入數字棧
- else if c is a number:
- nums.push(c)
- //若為操作符
- else if c is an operator:
- //若運算子棧的棧頂元素比c的優先順序高或一樣高,則進行單次運算
- while ops.top() is equal or precedence over c:
- op = ops.pop()
- opn2 = nums.pop()
- opn1 = nums.pop()
- //進行單次運算,並把運算數入數字棧
- nums.push( cal(op,opn1,opn2) )
- //若為右括弧
- else if c is ')':
- //除非棧頂元素為左括弧,否則運算子棧出棧並將計算結果入數字棧
- op = ops.pop()
- while op != '(':
- opn2 = nums.pop()
- opn1 = nums.pop()
- nums.push( cal(op,opn1,opn2) )
- op = ops.pop()
- //返回數字棧的棧頂元素
- return nums.top()
以下是運算式求值的DEMO:
函數調用
我們在調試代碼的時候,碰到函數報錯時經常會發現如下類似的報錯提示:
- /Users/tim/Codes/JavaScript/dsaginjs/DSinJS/Stack/InfixExpression.js:59
- return prioty[a] ) prioty[b];
- ^
- SyntaxError: Unexpected token )
- at Module._compile (module.js:439:25)
- at Object.Module._extensions..js (module.js:474:10)
- at Module.load (module.js:356:32)
- at Function.Module._load (module.js:312:12)
- at Function.Module.runMain (module.js:497:10)
- at startup (node.js:119:16)
- at node.js:902:3
其實我們只是在一處出錯了,為什麼會列印出這麼多報錯資訊呢?
原因就在於解譯器把報錯的函數經過的所有調用函數的棧列印了出來。
在函數調用的時候,我們需要切換到被調用的函數上,但是一旦函數調用結束,我們還得回到原來的位置。
利用棧,我們可以有條不紊的實現這點,即在函數調用的時候,我們把當前函數的變數和上下文入棧,等函數調用結束,我們再出棧,擷取之前的上下文和變數。