學習數據結構和算法十分重要。首要緣由是數據結構和算法能夠很高效地解決常見問題,這對從此的代碼質量相當重要(也包括性能,要是用了不恰當的數據結構或算法,極可能會產生性能問題)。其次,對於計算機科學,算法是最基礎的概念。數組是計算機科學中最經常使用的數據結構,咱們知道,能夠在數組的任意位置上刪除或添加元素。然而,有時候還須要一種在添加或刪除元素時有更多控制的數據結構。有兩種數據結構相似於數組,但在添加和刪除元素時更爲可控。它們就是棧和隊列。本文將詳細介紹棧git
棧是一種聽從後進先出(LIFO)原則的有序集合。新添加的或待刪除的元素都保存在棧的末尾,稱做棧頂,另外一端就叫棧底。在棧裏,新元素都靠近棧頂,舊元素都接近棧底。算法
在現實生活中也能發現不少棧的例子。例如,下圖裏的一摞書或者餐廳裏堆放的盤子編程
棧也被用在編程語言的編譯器和內存中保存變量、方法調用等數組
下面將建立一個類來表示棧,先聲明這個類:數據結構
function Stack() { //各類屬性和方法的聲明 }
使用一種數據結構來保存棧裏的元素。能夠選擇數組:閉包
let items = [];
接下來,爲棧聲明一些方法數據結構和算法
push(element(s)):添加一個(或幾個)新元素到棧頂
pop():移除棧頂的元素,同時返回被移除的元素
peek():返回棧頂的元素,不對棧作任何修改(這個方法不會移除棧頂的元素,僅僅返回它)
isEmpty():若是棧裏沒有任何元素就返回true,不然返回false
clear():移除棧裏的全部元素
size():返回棧裏的元素個數。這個方法和數組的length屬性很相似
【push】編程語言
push方法負責往棧裏添加新元素,有一點很重要:該方法只添加元素到棧頂,也就是棧的末尾ide
由於使用了數組來保存棧裏的元素,因此能夠數組的push方法來實現函數
this.push = function(element){ items.push(element); };
【pop】
接着來實現pop方法。這個方法主要用來移除棧裏的元素。棧聽從LIFO原則,所以移出的是最後添加進去的元素。所以,能夠用數組的pop方法
this.pop = function(){ return items.pop(); };
只能用push和pop方法添加和刪除棧中元素,這樣一來,棧天然就聽從了LIFO原則
【peek】
如今,爲類實現一些額外的輔助方法。若是想知道棧裏最後添加的元素是什麼,能夠用peek方法。這個方法將返回棧頂的元素:
this.peek = function(){ return items[items.length-1]; };
由於類內部是用數組保存元素的,因此訪問數組的最後一個元素能夠用 length - 1
在上圖中,有一個包含三個元素的棧,所以內部數組的長度就是3。數組中最後一項的位置是2,length - 1(3 -1)正好是2
【isEmpty】
下面要實現的方法是 isEmpty,若是棧爲空的話將返回true,不然就返回false:
this.isEmpty = function(){ return items.length == 0; };
使用isEmpty方法,能簡單地判斷內部數組的長度是否爲0
【size】
相似於數組的length屬性,也能實現棧的length。對於集合,最好用size代替length。由於棧的內部使用數組保存元素,因此能簡單地返回棧的長度:
this.size = function(){ return items.length; };
【clear】
最後來實現clear方法。clear方法用來移除棧裏全部的元素,把棧清空。實現這個方法最簡單的方式是:
this.clear = function(){ items = []; };
另外也能夠屢次調用pop方法,把數組中的元素所有移除,這樣也能實現clear方法
棧已經實現。經過一個例子來應用它,爲了檢查棧裏的內容,咱們來實現一個輔助方法,叫print。它會把棧裏的元素都輸出到控制檯:
this.print = function(){ console.log(items.toString()); };
這樣,咱們就完整建立了棧!
棧的完整代碼以下
function Stack() { let items = []; this.push = function(element){ items.push(element); }; this.pop = function(){ return items.pop(); }; this.peek = function(){ return items[items.length-1]; }; this.isEmpty = function(){ return items.length == 0; }; this.size = function(){ return items.length; }; this.clear = function(){ items = []; }; this.print = function(){ console.log(items.toString()); }; this.toString = function(){ return items.toString(); }; }
下面來學習如何使用Stack類。 首先,須要初始化Stack類。而後,驗證一下棧是否爲空(輸出是true,由於尚未往棧裏添加元素)
var stack = new Stack(); console.log(stack.isEmpty()); //輸出爲true
接下來,往棧裏添加一些元素(能夠添加任意類型的元素)
stack.push(5); stack.push(8);
若是調用peek方法,將會輸出8,由於它是往棧裏添加的最後一個元素:
console.log(stack.peek());//輸出8
再添加一個元素:
stack.push(11); console.log(stack.size()); //輸出3 console.log(stack.isEmpty()); //輸出false
咱們往棧裏添加了11。若是調用size方法,輸出爲3,由於棧裏有三個元素(五、8和11)。 若是調用isEmpty方法,會看到輸出了false(由於棧裏有三個元素,不是空棧)。最後, 咱們再添加一個元素:
stack.push(15);
下圖描繪了目前爲止咱們對棧的操做,以及棧的當前狀態:
而後,調用兩次pop方法從棧裏移除2個元素:
stack.pop(); stack.pop(); console.log(stack.size()); //輸出2 stack.print(); //輸出[5, 8]
在兩次調用pop方法前,咱們的棧裏有四個元素。調用兩次後,如今棧裏僅剩下5和8了。下圖描繪這個過程的執行:
下面來花點時間分析一下代碼,看看是否能用ES6的新功能來改進
咱們建立了一個能夠看成類來使用的Stack函數。JS函數都有構造函數,能夠用來模擬類的行爲。咱們聲明瞭一個私有的items變量,它只能被Stack函數/類訪問。然而,這個方法爲每一個類的實例都建立一個items變量的副本。所以,若是要建立多個Stack實例,它就不太適合了
下面用ES6新語法來聲明Stack類
class Stack { constructor () { this.items = []; } push(element){ this.items.push(element); } //其餘方法 }
咱們只是用ES6的簡化語法把Stack函數轉換成Stack類。這種方法不能像其餘語言(Java、C++、C#)同樣直接在類裏面聲明變量,只能在類的構造函數constructor裏聲明,在類的其餘函數裏用this.items就能夠引用這個變量
儘管代碼看起來更簡潔、更漂亮,變量items倒是公共的。ES6的類是基於原型的,雖然基於原型的類比基於函數的類更節省內存,也更適合建立多個實例,卻不能聲明私有屬性(變量)或方法。並且,在這種狀況下,咱們但願Stack類的用戶只能訪問暴露給類的方法。不然,就有可能從棧的中間移除元素(由於咱們用數組來存儲其值),這不是咱們但願看到的
ES6語法有沒有其餘方法來建立私有屬性呢?
【Symbol】
ES6新增了一種叫做Symbol的基本類型,它是不可變的,能夠用做對象的屬性。看看怎麼用它來在Stack類中聲明items屬性
let _items = Symbol(); //{1} class Stack { constructor () { this[_items] = []; //{2} } //Stack方法 }
在上面的代碼中,咱們聲明瞭Symbol類型的變量_items(行{1}),在類的constructor函數中初始化它的值(行{2})。要訪問_items,只需把全部的this.items都換成this[_items]
這種方法建立了一個假的私有屬性,由於ES6新增的Object.getOwnPropertySymbols方法可以取到類裏面聲明的全部Symbols屬性。下面是一個破壞Stack類的例子:
let stack = new Stack(); stack.push(5); stack.push(8); let objectSymbols = Object.getOwnPropertySymbols(stack); console.log(objectSymbols.length); // 1 console.log(objectSymbols); // [Symbol()] console.log(objectSymbols[0]); // Symbol() stack[objectSymbols[0]].push(1); stack.print(); //輸出 5, 8, 1
從以上代碼能夠看到,訪問stack[objectSymbols[0]]是能夠獲得_items的。而且,_items屬性是一個數組,能夠進行任意的數組操做,好比從中間刪除或添加元素。咱們操做的是棧,不該該出現這種行爲
【WeakMap】
有一種數據類型能夠確保屬性是私有的,這就是WeakMap。WeakMap能夠存儲鍵值對,其中鍵是對象,值能夠是任意數據類型。
若是用WeakMap來存儲items變量,Stack類就是這樣的:
const items = new WeakMap(); //{1} class Stack { constructor () { items.set(this, []); //{2} } push(element) { let s = items.get(this); //{3} s.push(element); } pop() { let s = items.get(this); let r = s.pop(); return r; } //其餘方法 }
行{1},聲明一個WeakMap類型的變量items。行{2},在constructor中,以this(Stack類本身的引用)爲鍵,把表明棧的數組存入items。行{3},從WeakMap中取出值,即以this爲鍵(行{2}設置的)從items中取值
如今知道,items在Stack類裏是真正的私有屬性了,但還有一件事要作。items如今仍然是在Stack類之外聲明的,所以誰均可以改動它。要用一個閉包(外層函數)把Stack類包起來,這樣就只能在這個函數裏訪問WeakMap:
let Stack = (function () { const items = new WeakMap(); class Stack { constructor () { items.set(this, []); } //其餘方法 } return Stack; //{5} })();
當Stack函數裏的構造函數被調用時,會返回Stack類的一個實例(行{5})
如今,Stack類有一個名爲items的私有屬性。雖然它很醜陋,但畢竟實現了私有屬性。然而,用這種方法的話,擴展類沒法繼承私有屬性。魚與熊掌不可兼得
棧的完整代碼以下
let Stack3 = (function () { const items = new WeakMap(); class Stack3 { constructor () { items.set(this, []); } push(element){ let s = items.get(this); s.push(element); } pop(){ let s = items.get(this); let r = s.pop(); return r; } peek(){ let s = items.get(this); return s[s.length-1]; } isEmpty(){ return items.get(this).length == 0; } size(){ let s = items.get(this); return s.length; } clear(){ items.set(this, []); } print(){ console.log(this.toString()); } toString(){ return items.get(this).toString(); } } return Stack3; })();
把上面的代碼跟最初實現的Stack類作個比較,咱們會發現有一些類似之處:
function Stack() { let items = []; //其餘方法 }
事實上,儘管ES6引入了類的語法,仍然不能像在其餘編程語言中同樣聲明私有屬性或方法。有不少種方法均可以達到相同的效果,但不管是語法仍是性能,這些方法都有各自的優勢和缺點
哪一種方法更好?這取決於在實際項目中如何使用算法,要處理的數據量,要建立的實例個數,以及其餘約束條件
棧的實際應用很是普遍。在回溯問題中,它能夠存儲訪問過的任務或路徑、撤銷的操做。Java和C#用棧來存儲變量和方法調用,特別是處理遞歸算法時,有可能拋出一個棧溢出異常
下面將學習使用棧的三個最著名的算法示例。首先是十進制轉二進制問題,以及任意進制轉換的算法;而後是平衡圓括號問題;最後,學習如何用棧解決漢諾塔問題
【十進制轉二進制】
現實生活中,咱們主要使用十進制。但在計算科學中,二進制很是重要,由於計算機裏的全部內容都是用二進制數字表示的(0和1)。沒有十進制和二進制相互轉化的能力,與計算機交流就很困難
要把十進制轉化成二進制,咱們能夠將該十進制數字和2整除(二進制是滿二進一),直到結果是0爲止。舉個例子,把十進制的數字10轉化成二進制的數字,過程大概是這樣
下面是對應的算法描述:
function divideBy2(decNumber){ var remStack = new Stack(), rem, binaryString = ''; while (decNumber > 0){ //{1} rem = Math.floor(decNumber % 2); //{2} remStack.push(rem); //{3} decNumber = Math.floor(decNumber / 2); //{4} } while (!remStack.isEmpty()){ //{5} binaryString += remStack.pop().toString(); } return binaryString; }
在這段代碼裏,當結果知足和2作整除的條件時(行{1}),咱們會得到當前結果和2的餘數,放到棧裏(行{2}、{3})。而後讓結果和2作整除(行{4})。另外請注意:JavaScript有數字類型,可是它不會區分到底是整數仍是浮點數。所以,要使用Math.floor函數讓除法的操做僅返回整數部分。最後,用pop方法把棧中的元素都移除,把出棧的元素變成鏈接成字符串(行{5})。
用剛纔寫的算法作一些測試,使用如下代碼把結果輸出到控制檯裏:
console.log(divideBy2(233)); //輸出11101001 console.log(divideBy2(10)); //輸出1010 console.log(divideBy2(1000)); //輸出1111101000
【進制轉換算法】
咱們很容易修改以前的算法,使之能把十進制轉換成任何進制。除了讓十進制數字和2整除 轉成二進制數,還能夠傳入其餘任意進制的基數爲參數,就像下面算法這樣:
function baseConverter(decNumber, base){ var remStack = new Stack(), rem, baseString = '', digits = '0123456789ABCDEF'; //{6} while (decNumber > 0){ rem = Math.floor(decNumber % base); remStack.push(rem); decNumber = Math.floor(decNumber / base); } while (!remStack.isEmpty()){ baseString += digits[remStack.pop()]; //{7} } return baseString; }
咱們只須要改變一個地方。在將十進制轉成二進制時,餘數是0或1;在將十進制轉成八進制時,餘數是0到7之間的數;可是將十進制轉成16進制時,餘數是0到9之間的數字加上A、B、C、D、E和F(對應十、十一、十二、1三、14和15)。所以,咱們須要對棧中的數字作個轉化才能夠(行{6}和行{7})
可使用以前的算法,輸出結果以下:
console.log(baseConverter(100345, 2)); //輸出11000011111111001 console.log(baseConverter(100345, 8)); //輸出303771 console.log(baseConverter(100345, 16)); //輸出187F9
【平衡圓括號】
function parenthesesChecker(symbols){ let stack = new Stack(), balanced = true, index = 0, symbol, top, opens = "([{", closers = ")]}"; while (index < symbols.length && balanced){ symbol = symbols.charAt(index); if (opens.indexOf(symbol) >= 0){ stack.push(symbol); console.log(`open symbol - stacking ${symbol}`); } else { console.log(`close symbol ${symbol}`); if (stack.isEmpty()){ balanced = false; console.log('Stack is empty, no more symbols to pop and compare'); } else { top = stack.pop(); //if (!matches(top, symbol)){ if (!(opens.indexOf(top) === closers.indexOf(symbol))) { balanced = false; console.log(`poping symbol ${top} - is not a match compared to ${symbol}`); } else { console.log(`poping symbol ${top} - is is a match compared to ${symbol}`); } } } index++; } if (balanced && stack.isEmpty()){ return true; } return false; } console.log(parenthesesChecker('{([])}')); //true console.log(parenthesesChecker('{{([][])}()}')); //true console.log(parenthesesChecker('[{()]')); //false
【漢諾塔】
function towerOfHanoi(n, from, to, helper){ if (n > 0){ towerOfHanoi(n-1, from, helper, to); to.push(from.pop()); console.log('-----'); console.log('Source: ' + from.toString()); console.log('Dest: ' + to.toString()); console.log('Helper: ' + helper.toString()); towerOfHanoi(n-1, helper, to, from); } } var source = new Stack(); source.push(3); source.push(2); source.push(1); var dest = new Stack(); var helper = new Stack(); towerOfHanoi(source.size(), source, dest, helper); source.print(); helper.print(); dest.print();