棧(stack)又名堆疊,它是一種運算受限的線性表。其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱為棧頂,相對地,把另一端稱為棧底。
一、實現一個棧類Stack
基於堆疊的特性,可以用陣列做線性表進行儲存。
初始化Stack
類的結構如下:
function Stack(){
this.space = [];
}
Stack.prototype = {
constructor: Stack,
/* 介面code */
};
複製程式碼
接下來,就是在原型上,對入棧
、出棧
、清空棧
、讀取棧頂
、讀取整個棧資料
這幾個介面的實現。
Stack
類預設以陣列頭部做棧底,尾部做棧頂。
1.1 入棧 push
入棧可以利用js陣列的push
方法,在陣列尾部壓入資料。
Stack.prototype = {
push: function(value){
return this.space.push(value);
}
}
複製程式碼
1.2 出棧 pop
出棧同樣是利用js陣列的pop
方法,在陣列尾部推出資料。
Stack.prototype = {
pop: function(){
return this.space.pop();
}
}
複製程式碼
1.3 清空棧 clear
清空棧相對簡單,將儲存資料的陣列重置為空陣列即可。
Stack.prototype = {
clear: function(){
this.space = [];
}
}
複製程式碼
1.4 讀取棧頂readTop
讀取棧頂資料,採用陣列下標的方式進行獲取。帶來的一個好處就是:下標超出陣列有效範圍時,返回值為undefined
。
Stack.prototype = {
readTop: function(){
return this.space[this.space.length - 1];
}
}
複製程式碼
1.4 讀取整個棧read
讀取整個棧資料,直接返回當前陣列即可。
Stack.prototype = {
read: function(){
return this.space;
}
}
複製程式碼
1.5 聚合
最後,將所有功能聚合後,如下所示,一個堆疊的資料結構就搞定了。
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;
}
};
複製程式碼
二、實戰
學資料結構和演算法是為了更好、更高效率地解決工程問題。 這裡學以致用,提供了幾個真實的案例,來體會下資料結構和演算法的魅力:)
2.1 陣列reverse
的實現
當前案例,將用堆疊來實現陣列的反轉功能。
function reverse(arr){
var ArrStack = new Stack();
for(var i = arr.length - 1; i >= 0; i--){
ArrStack.push(arr[i]);
}
return ArrStack.read();
}
複製程式碼
如程式碼所示,可分為以下幾個步驟:
- 例項化一個堆疊用於儲存資料
- 將傳入的陣列進行倒序遍歷,並逐個壓入堆疊
- 最後使用
read
介面,輸出資料
好像很簡單,不用擔心,複雜的在後面:)
2.2 十進位制轉換為二進位制
數值轉換進位制的問題,是堆疊的小試牛刀。
講解轉換方法前,先來看一個小例子:
將十進位制的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"
複製程式碼
2.3 表示式求值
這個案例,其實可以理解為簡化版的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
。由於乘除運算子前後的數字,在運算上有區別,所以不能隨意調換位置。
2.4 中綴表示式轉換為字尾表示式(逆波蘭表示法)
逆波蘭表示法,是一種對計算機友好的表示法,不需要使用括號。
下面案例,是對上一個案例的變通,也是用排程場演算法
,將中綴表示式轉換為字尾表示式。
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 - * +"
複製程式碼
2.5 漢諾塔
漢諾塔(港臺:河內塔)是根據一個傳說形成的數學問題:
有三根杆子A,B,C。A杆上有 N 個 (N>1) 穿孔圓盤,盤的尺寸由下到上依次變小。要求按下列規則將所有圓盤移至 C 杆:
- 每次只能移動一個圓盤;
- 大盤不能疊在小盤上面。
堆疊的經典演算法應用,首推就是漢諾塔
。
理解該演算法,要注意以下幾點:
- 不要深究每次的移動,要抽象理解
- 第一步:所有不符合要求的盤,從A塔統一移到B塔快取
- 第二步:將符合的盤移動到C塔
- 第三步:把B塔快取的盤全部移動到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] 漢諾塔
喜歡我文章的朋友,可以通過以下方式關注我:
- 「star」 或 「watch」 我的GitHub blog
- RSS訂閱我的個人部落格:王先生的基地