為什麼我要放棄javaScript資料結構與演算法(第三章)—— 棧
有兩種結構類似於陣列,但在新增和刪除元素時更加可控,它們就是棧和佇列。
第三章 棧
棧資料結構
棧是一種遵循後進先出(LIFO)原則的有序集合。新新增的或待刪除的元素都儲存在棧的同一端,稱為棧頂,另一端就叫做棧底。在棧裡, 新元素都靠近棧頂,舊元素都接近棧底。
棧也被用在程式語言的編譯器和記憶體中儲存變數、方法呼叫等。
建立棧
- 先宣告這個類
function Stack(){
// 各種屬性和方法的宣告
}
- 選擇陣列這種資料結構來儲存棧裡的元素
let items = [];
為棧宣告一些方法
- push(element(s)): 新增一個(或者幾個)新元素到棧頂
- pop():移除棧頂的元素,同時返回被移除的元素
- peek():返回棧頂的元素,不會對棧做任何修改(這個方法不會移除棧頂的元素,僅僅返回它)
- isEmpty():如果棧裡沒有任何元素的就返回true,否則就返回false.
- clear():移除棧裡的所有元素
- size():返回棧裡的元素個數,這個方法和陣列的length屬性很類似。
向棧新增元素
我們要實現的第一個方法是 push,這個方法負責向棧裡新增新元素,該方法只新增元素到棧頂,也就是棧的末尾。
this.push = function(element){ return items.push(element); }
只能用 push 和 pop 方法新增和刪除棧中元素,這樣一來,我們的棧就自然遵從了 LIFO 原則。
向棧移除元素
我們要實現的第一個方法是 pop,這個方法主要用來移除棧裡的元素。棧遵從 LIFO 原則,因此移出的是最後新增進去的元素。棧的 pop 方法可以這麼寫
this.pop = function(){
return items.pop();
}
只能用 push 和 pop 方法新增和刪除棧中元素,這樣一來,我們的棧就自然遵從了 LIFO 原則。
檢視棧頂元素
現在為類實現一些額外的輔助方法,如果想知道棧裡最後新增的元素是什麼,可以用 peek 方法,這個方法將返回棧頂的元素。
this.peek = function(){
return items[items.length-1];
}
因為類內部是用陣列儲存元素的,所以訪問陣列的最後一個元素可以用 length - 1
檢查棧是否為空
isEmpty ,如果棧為空的話就返回true,否則就返回false
this.isEmpty = function(){
return items.length == 0;
}
類似於陣列的 length 屬性,我們也能實現棧的 length,對於集合,最好用 size 代替 length。因為棧的內部使用陣列儲存元素,所以能簡單地返回棧的長度。
this.size = function(){
return items.length;
}
清空和列印棧元素
實現 clear 方法。clear 方法用來移除棧裡所有的元素,把棧清空。實現這個方法最簡單的方式是
this.clear = function(){
items = [];
return null;
}
打印出來棧裡面的內容,通過實現輔助方法 print 來實現。
this.print = function(){
console.log(items.toString());
}
例項
function Stack(){
let items = [];
this.push = function(element){
return 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());
}
}
let stack = new Stack();
console.log(stack.isEmpty()); // true 判斷是否為空
stack.push(5); // 往棧裡新增元素 5
stack.push(8); // 往棧裡新增元素 8
console.log(stack.peek()); // 檢視最後一個元素 8
stack.push(11); // 往棧裡新增元素 11
console.log(stack.size()); // 3 輸出棧的元素個數
console.log(stack.isEmpty()); // false 判斷是否為空
stack.push(15); // 往棧裡新增元素 15
stack.print(); // 5,8,11,15 輸出棧裡的元素
下面是流程圖
ECMAScript6 和 Stack 類
建立了一個可以當做類來使用的 Stack 函式。JavaScript 函式都有建構函式,可以用來模擬類的行為。我們宣告一個私有的 items變數,它只能被 Stack 函式/類訪問。然而,這個方法為每個類的例項都建立了一個 items 變數的副本。因此如果要建立多個 Stack例項,就不太適合。我們可以嘗試用 ES6語法來宣告 Stack 類。
用 ES6 宣告 Stack 類
class Stack{
constructor(){
this.items = []; // {1}
}
push(elememt){
this.items.push(element);
}
// 其他方法
}
只是用 ES6 的簡化語法把 Stack 函式轉換成 Stack 類。這種方法不能像其他語言(Java、C++、C#)一樣直接在類裡面宣告變數,只能在類的建構函式 constructor 裡宣告,在類的其他函式裡用 this.nameofVariable 就可以引用這個變數。
儘管程式碼看起來更加簡潔、更漂亮,變數 items 卻是公共的。ES6 類是基於原型的。雖然基於原型的類比基於函式的類更節省記憶體,也更適合建立多個例項,卻不能夠宣告私有屬性(變數)或方法。而且,在這種情況下,我們希望 Stack 類的使用者只能訪問暴露給類的方法。否則,就有可能從棧的中間移除元素(因為我們用陣列來儲存其值),這不是我們希望看到的。
用ES6的限定作用域 Symbol 實現類
ES6 新增了一種叫做 Symbol 的基本型別,它是不可變的,可以用作物件的屬性。
let _items = Symbol(); // 聲明瞭 Symbol 型別的變數
class Stack{
constructor(){
this[_items] = [] // 要訪問 _items,只需把所有的 this.items都換成 this.[_items]
}
push(element){
return this[_items].push(element);
}
pop (){
return this[_items].pop();
}
peek (){
return this[_items][this[_items].length-1];
}
isEmpty (){
return this[_items].length == 0;
}
size (){
return this[_items].length;
}
clear (){
this[_items] = [];
}
print (){
console.log(this[_items].toString());
}
}
這種方法建立了一個假的私有屬性,因為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[objectSymbol[0]] 得到 _items。並且 _items屬性是一個數組,可以進行任意的陣列操作,比如從中間刪除或者是新增元素。我們操作的是棧,不應該有這種行為出現。
用ES6類的 WeakMap 實現類
有一種資料型別可確保屬性是私有的,這就是 WeakMap。後面會深入探討 Map 這種資料結構,現在只需要知道 WeakMap 可以儲存鍵值對,其中鍵是物件,值可以是任意資料型別。
如果使用 WeakMap 來儲存 items 變數,那麼 Stack 類是這樣的
const items = new WeakMap(); // 聲明瞭一個 WeakMap 型別的變數 items
class Stack{
constructor(){
items.set(this, []) // 在 constructor 中,以this(Stack類自己引用)為鍵,把代表棧的陣列存入 items
}
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 (){
let s = items.get(this);
return s.length == 0;
}
size (){
let s = items.get(this);
let r = s.length
return r;
}
clear (){
items.set(this, [])
}
print (){
let s = items.get(this);
console.log(s.toString());
}
}
現在 items 在 Stack 類裡是真正的私有屬性了,但是還有一件事要做, items 現在仍然是在 Stack 類以外宣告的,因此任何誰都可以改動它。我們可以用一個閉包(外層函式)把 Stack 類包起來,這樣就可以在這個函式裡訪問 WeakMap
let stack = (function(){
const items = new WeakMap();
class Stack {
constructor(){
items.set(this, []);
}
// 其他方法
}
return Stack; // 當 Stack 函式裡的建構函式被呼叫時,會返回 Stack 類的一個例項。
})()
現在,Stack 類有一個名為 items 的私有屬性。然後用這種方法的話,擴充套件類無法繼承其屬性。將其與最開始用 function 實現的 Stack 類來做個比較,我們會發現一些相似之處。
事實上,儘管 ES6 引入了類的語法,我們仍然不能像在其他程式語言中一樣宣告私有屬性或方法。有很多種方法都可以達到相同的效果,但無論是語法還是效能,這些方法都有各自的缺點和優點。
用棧解決問題
棧的實際應用非常廣泛。在回溯問題中,它可以儲存訪問過的任務或是路徑、撤銷的操作。Java 和 C# 用棧來儲存變數和方法呼叫,特別是處理遞迴演算法時,有可能丟擲一個棧溢位異常(stack overflow)
下面,學習使用棧的三個最著名的演算法例項。首先是十進位制轉二進位制的問題,以及任意進位制轉換的演算法,然後是平衡圓括號問題,最後,會學習棧解決漢諾塔的問題。
從十進位制到二進位制
計算科學中,二進位制非常重要,因為計算機裡的所有內容都是用二進位制數字表示(0和1)。沒有十進位制和二進位制相互轉化的能力,與計算機交流就很困難。要把十進位制化成十進位制,將該十進位制數字和2整除,直到結果為0為止。
例項:數字10轉為二進位制的數字。
function divideBy2(decNumber){
var remStack = new Stack(),
rem,
binaryString = '';
while(decNumber > 0){
rem = Math.floor(decNumber % 2); // 拿到被2整除的餘數
remStack.push(rem);
decNumber = Math.floor(decNumber / 2) // 拿到被2整除的整數
}
while (! remStack.isEmpty()){
binaryString += remStack.pop().toString();
}
return binaryString;
}
console.log(divideBy2(10)); // 1010
console.log(divideBy2(233)); // 11101001
console.log(divideBy2(100)); // 11101001
JavaScript有數字型別,但是不會區分究竟是整數還是浮點數,使用 Math.floor 讓除法只返回整數部分。
進位制轉換演算法
可以傳入任意進位制的基數作為引數
function baseConverter(decNumber, base){
var remStack = new Stack(),
rem,
baseString = '',
digits = '0123456789ABCDEF';
while(decNumber > 0){
rem = Math.floor(decNumber % base); // 拿到被base整除的餘數
remStack.push(rem);
decNumber = Math.floor(decNumber / base) // 拿到被base整除的整數
}
while (! remStack.isEmpty()){
baseString += digits[remStack.pop()];
}
return baseString;
}
console.log(baseConverter(100345,2)); // 11000011111111001
console.log(baseConverter(100345, 8)); // 303771
console.log(baseConverter(100345, 16)); // 187F9
需要改動的地方:在將十進位制轉為二進位制的時候,餘數是0或者1,轉為八進位制的時候,餘數為0~7,同理16進位制是0~9加上A~F。所以要做個轉換,通過定義 digits ,digits[remStack.pop()] 來實現轉化。
小結
通過這一章,學習了棧這一資料結構的相關內容。可以用程式碼自己實現棧,還講解了棧裡面的相關方法。
書籍連結: 學習JavaScript資料結構與演算法