有兩種結構類似於數組,但在添加和刪除元素時更加可控,它們就是棧和隊列。
第三章 棧
棧數據結構
棧是一種遵循后進先出(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數據結構與算法