棧(stack)又名堆棧,它是一種運算受限的線性表。其限制是 僅容許在表的一端進行插入和刪除運算。這一端被稱爲棧頂,相對地,把另外一端稱爲棧底。
基於堆棧的特性,能夠用數組作線性表進行存儲。
初始化Stack
類的結構以下:javascript
function Stack(){ this.space = []; } Stack.prototype = { constructor: Stack, /* 接口code */ };
接下來,就是在原型上,對入棧
、出棧
、清空棧
、讀取棧頂
、讀取整個棧數據
這幾個接口的實現。Stack
類默認以數組頭部作棧底,尾部作棧頂。java
push
入棧能夠利用js數組的push
方法,在數組尾部壓入數據。git
Stack.prototype = { push: function(value){ return this.space.push(value); } }
pop
出棧一樣是利用js數組的pop
方法,在數組尾部推出數據。github
Stack.prototype = { pop: function(){ return this.space.pop(); } }
clear
清空棧相對簡單,將存儲數據的數組重置爲空數組便可。算法
Stack.prototype = { clear: function(){ this.space = []; } }
readTop
讀取棧頂數據,採用數組下標的方式進行獲取。帶來的一個好處就是:下標超出數組有效範圍時,返回值爲undefined
。數組
Stack.prototype = { readTop: function(){ return this.space[this.space.length - 1]; } }
read
讀取整個棧數據,直接返回當前數組便可。緩存
Stack.prototype = { read: function(){ return this.space; } }
最後,將全部功能聚合後,以下所示,一個堆棧的數據結構就搞定了。數據結構
function Stack(){ this.space = []; } Stack.prototype = { constructor: Stack, push: function(value){ return this.space.push(value); }, pop: function(){ return this.space.pop(); }, clear: function(){ this.space = []; }, readTop: function(){ return this.space[this.space.length - 1]; }, read: function(){ return this.space; } };
學數據結構和算法是爲了更好、更高效率地解決工程問題。
這裏學以至用,提供了幾個真實的案例,來體會下數據結構和算法的魅力:)數據結構和算法
reverse
的實現當前案例,將用堆棧來實現數組的反轉功能。this
function reverse(arr){ var ArrStack = new Stack(); for(var i = arr.length - 1; i >= 0; i--){ ArrStack.push(arr[i]); } return ArrStack.read(); }
如代碼所示,可分爲如下幾個步驟:
read
接口,輸出數據好像很簡單,不用擔憂,複雜的在後面:)
數值轉換進制的問題,是堆棧的小試牛刀。
講解轉換方法前,先來看一個小例子:
將十進制的13轉換成二進制
2 | 13 1  ̄ ̄ ̄ 2 | 6 0  ̄ ̄ ̄ 2 | 3 1  ̄ ̄ ̄ ̄ 1 1
如上所示:13的二進制碼爲1101
。
將手工換算,變成堆棧存儲,只需將對2取餘的結果依次壓入堆棧保存,最後反轉輸出便可。
function binary(number){ var tmp = number; var ArrStack = new Stack(); if(number === 0){ return 0; } while(tmp){ ArrStack.push(tmp % 2); tmp = parseInt(tmp / 2, 10); } return reverse(ArrStack.read()).join(''); } binary(14); // 輸出=> "1110" binary(1024); // 輸出=> "10000000000"
這個案例,其實能夠理解爲簡化版的eval
方法。
案例內容是對1+7*(4-2)
的求值。
進入主題前,有必要先了解如下的數學理論:
- 中綴表示法(或中綴記法)是一個通用的算術或邏輯公式表示方法, 操做符是以中綴形式處於操做數的中間(例:3 + 4)。
- 逆波蘭表示法(Reverse Polish notation,RPN,或逆波蘭記法),是一種是由波蘭數學家揚·武卡謝維奇1920年引入的數學表達式方式,在逆波蘭記法中,全部操做符置於操做數的後面,所以也被稱爲後綴表示法。逆波蘭記法不須要括號來標識操做符的優先級。
常規中綴記法的「3 - 4 + 5」在逆波蘭記法中寫做「3 4 - 5 +」
- 調度場算法(Shunting Yard Algorithm)是一個用於將中綴表達式轉換爲後綴表達式的經典算法,由艾茲格·迪傑斯特拉引入,因其操做相似於火車編組場而得名。
提早說明,這只是簡單版實現。因此規定有兩個:
- 數字要求爲整數
- 不容許表達式中出現多餘的空格
實現代碼以下:
function calculate(exp){ var valueStack = new Stack(); // 數值棧 var operatorStack = new Stack(); // 操做符棧 var expArr = exp.split(''); // 切割字符串表達式 var FIRST_OPERATOR = ['+', '-']; // 加減運算符 var SECOND_OPERATOR = ['*', '/']; // 乘除運算符 var SPECIAL_OPERATOR = ['(', ')']; // 括號 var tmp; // 臨時存儲當前處理的字符 var tmpOperator; // 臨時存儲當前的運算符 // 遍歷表達式 for(var i = 0, len = expArr.length; i < len; i++){ tmp = expArr[i]; switch(tmp){ case '(': operatorStack.push(tmp); break; case ')': // 遇到右括號,先出棧括號內數據 while( (tmpOperator = operatorStack.pop()) !== '(' && typeof tmpOperator !== 'undefined' ){ valueStack.push(calculator(tmpOperator, valueStack.pop(), valueStack.pop())); } break; case '+': case '-': while( typeof operatorStack.readTop() !== 'undefined' && SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 && (SECOND_OPERATOR.indexOf(operatorStack.readTop()) !== -1 || tmp != operatorStack.readTop()) ){ // 棧頂爲乘除或相同優先級運算,先出棧 valueStack.push(calculator(operatorStack.pop(), valueStack.pop(), valueStack.pop())); } operatorStack.push(tmp); break; case '*': case '/': while( typeof operatorStack.readTop() != 'undefined' && FIRST_OPERATOR.indexOf(operatorStack.readTop()) === -1 && SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 && tmp != operatorStack.readTop()){ // 棧頂爲相同優先級運算,先出棧 valueStack.push(calculator(operatorStack.pop(), valueStack.pop(), valueStack.pop())); } operatorStack.push(tmp); break; default: valueStack.push(tmp); } } // 處理棧內數據 while( typeof (tmpOperator = operatorStack.pop()) !== 'undefined' ){ valueStack.push(calculator(tmpOperator, valueStack.pop(), valueStack.pop())); } return valueStack.pop(); // 將計算結果推出 /* @param operator 操做符 @param initiativeNum 主動值 @param passivityNum 被動值 */ function calculator(operator, passivityNum, initiativeNum){ var result = 0; initiativeNum = typeof initiativeNum === 'undefined' ? 0 : parseInt(initiativeNum, 10); passivityNum = typeof passivityNum === 'undefined' ? 0 : parseInt(passivityNum, 10); switch(operator){ case '+': result = initiativeNum + passivityNum; console.log(`${initiativeNum} + ${passivityNum} = ${result}`); break; case '-': result = initiativeNum - passivityNum; console.log(`${initiativeNum} - ${passivityNum} = ${result}`); break; case '*': result = initiativeNum * passivityNum; console.log(`${initiativeNum} * ${passivityNum} = ${result}`); break; case '/': result = initiativeNum / passivityNum; console.log(`${initiativeNum} / ${passivityNum} = ${result}`); break; default:; } return result; } }
實現思路:
調度場算法
,對中綴表達式進行讀取,對結果進行合理運算。operatorStack.readTop() !== 'undefined'
進行斷定。有些書採用#
作結束標誌,我的以爲有點累贅。split
進行拆分,而後進行遍歷讀取,壓入堆棧。有提早要計算結果的,進行對應的出棧處理。calculator
。因爲乘除運算符先後的數字,在運算上有區別,因此不能隨意調換位置。逆波蘭表示法,是一種對計算機友好的表示法,不須要使用括號。
下面案例,是對上一個案例的變通,也是用調度場算法
,將中綴表達式轉換爲後綴表達式。
function rpn(exp){ var valueStack = new Stack(); // 數值棧 var operatorStack = new Stack(); // 操做符棧 var expArr = exp.split(''); var FIRST_OPERATOR = ['+', '-']; var SECOND_OPERATOR = ['*', '/']; var SPECIAL_OPERATOR = ['(', ')']; var tmp; var tmpOperator; for(var i = 0, len = expArr.length; i < len; i++){ tmp = expArr[i]; switch(tmp){ case '(': operatorStack.push(tmp); break; case ')': // 遇到右括號,先出棧括號內數據 while( (tmpOperator = operatorStack.pop()) !== '(' && typeof tmpOperator !== 'undefined' ){ valueStack.push(translate(tmpOperator, valueStack.pop(), valueStack.pop())); } break; case '+': case '-': while( typeof operatorStack.readTop() !== 'undefined' && SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 && (SECOND_OPERATOR.indexOf(operatorStack.readTop()) !== -1 || tmp != operatorStack.readTop()) ){ // 棧頂爲乘除或相同優先級運算,先出棧 valueStack.push(translate(operatorStack.pop(), valueStack.pop(), valueStack.pop())); } operatorStack.push(tmp); break; case '*': case '/': while( typeof operatorStack.readTop() != 'undefined' && FIRST_OPERATOR.indexOf(operatorStack.readTop()) === -1 && SPECIAL_OPERATOR.indexOf(operatorStack.readTop()) === -1 && tmp != operatorStack.readTop()){ // 棧頂爲相同優先級運算,先出棧 valueStack.push(translate(operatorStack.pop(), valueStack.pop(), valueStack.pop())); } operatorStack.push(tmp); break; default: valueStack.push(tmp); } } while( typeof (tmpOperator = operatorStack.pop()) !== 'undefined' ){ valueStack.push(translate(tmpOperator, valueStack.pop(), valueStack.pop())); } return valueStack.pop(); // 將計算結果推出 /* @param operator 操做符 @param initiativeNum 主動值 @param passivityNum 被動值 */ function translate(operator, passivityNum, initiativeNum){ var result = ''; switch(operator){ case '+': result = `${initiativeNum} ${passivityNum} +`; console.log(`${initiativeNum} + ${passivityNum} = ${result}`); break; case '-': result = `${initiativeNum} ${passivityNum} -`; console.log(`${initiativeNum} - ${passivityNum} = ${result}`); break; case '*': result = `${initiativeNum} ${passivityNum} *`; console.log(`${initiativeNum} * ${passivityNum} = ${result}`); break; case '/': result = `${initiativeNum} ${passivityNum} /`; console.log(`${initiativeNum} / ${passivityNum} = ${result}`); break; default:; } return result; } } rpn('1+7*(4-2)'); // 輸出=> "1 7 4 2 - * +"
漢諾塔(港臺:河內塔)是根據一個傳說造成的數學問題:
有三根杆子A,B,C。A杆上有 N 個 (N>1) 穿孔圓盤,盤的尺寸由下到上依次變小。要求按下列規則將全部圓盤移至 C 杆:
- 每次只能移動一個圓盤;
- 大盤不能疊在小盤上面。
堆棧的經典算法應用,首推就是漢諾塔
。
理解該算法,要注意如下幾點:
如下是代碼實現:
var ATower = new Stack(); // A塔 var BTower = new Stack(); // B塔 var CTower = new Stack(); // C塔 (目標塔) var TIER = 4; // 層數 for(var i = TIER; i > 0; i--){ ATower.push(i); } function Hanoi(n, from, to, buffer){ if(n > 0){ Hanoi(n - 1, from, buffer, to); // 全部不符合要求的盤(n-1),從A塔統一移到B塔緩存 to.push(from.pop()); // 將符合的盤(n)移動到C塔 Hanoi(n - 1, buffer, to, from); // 把B塔緩存的盤所有移動到C塔 } } Hanoi(ATower.read().length, ATower, CTower, BTower);
漢諾塔的重點,仍是靠遞歸去實現。把一個大問題,經過遞歸,不斷分拆爲更小的問題。而後,集中精力解決小問題便可。
不知不覺,寫得有點多ORZ。
後面章節的參考連接,仍是推薦看看。也許配合本文,你會有更深的理解。
[1] 中綴表示法
[2] 後綴表示法
[3] 調度場算法
[4] 漢諾塔
喜歡我文章的朋友,能夠經過如下方式關注我: