為什么我要放棄javaScript數據結構與算法(第三章)—— 棧


有兩種結構類似於數組,但在添加和刪除元素時更加可控,它們就是棧和隊列。

第三章 棧

棧數據結構

棧是一種遵循后進先出(LIFO)原則的有序集合。新添加的或待刪除的元素都保存在棧的同一端,稱為棧頂,另一端就叫做棧底。在棧里, 新元素都靠近棧頂,舊元素都接近棧底。

棧也被用在編程語言的編譯器和內存中保存變量、方法調用等。

創建棧

  1. 先聲明這個類
   function Stack(){
       // 各種屬性和方法的聲明
   }
  1. 選擇數組這種數據結構來保存棧里的元素
   let items = [];
  1. 為棧聲明一些方法

    • 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轉為二進制的數字。

數字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數據結構與算法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM