學習JavaScript數據結構與算法(第3版)閱讀筆記---第4章


4.2 棧數據結構

棧是一種遵從后進先出(LIFO)原則的有序集合。新添加或待刪除的元素都保存在棧的同 一端,稱作棧頂,另一端就叫棧底。在棧里,新元素都靠近棧頂,舊元素都接近棧底。
棧的使用場景:在編程語言的編譯器和內存中保存變量、方法調用等,也被用於瀏覽器歷史記錄 (瀏覽器的返回按鈕)。

4.2.1 創建一個基於數組的棧

創建一個類來表示棧。從創建一個 stack-array.js 文件並聲明 Stack 類開始。

class Stack { 
    constructor() { 
    this.items = []; // {1} 
    } 
}

選擇數組(行{1})來保存棧里的元素。由於棧遵循 LIFO 原則,需要對元素的插入和刪除功能進行限制。接下來, 要為棧聲明一些方法。
push(element(s)):添加一個(或幾個)新元素到棧頂。
pop():移除棧頂的元素,同時返回被移除的元素。
peek():返回棧頂的元素,不對棧做任何修改(該方法不會移除棧頂的元素,僅僅返回它)。
isEmpty():如果棧里沒有任何元素就返回 true,否則返回 false。
clear():移除棧里的所有元素。
size():返回棧里的元素個數。該方法和數組的 length 屬性很類似。

4.2.2 向棧添加元素

push方法負責往棧里添加新元素,有一點很重要:該方法只添加元素到棧頂,也就是棧的末尾。

push(element) { 
    this.items.push(element); 
}
4.2.3 從棧移除元素

pop 方法主要用來移除棧里的元素。棧遵從 LIFO 原則,因此移 出的是最后添加進去的元素。

pop() { 
    return this.items.pop(); 
}
4.2.4 查看棧頂元素

peek 方法將返回棧頂的元素(棧里最后添加的元素)。

peek() { 
    return this.items[this.items.length - 1]; 
}

4.2.5 檢查棧是否為空

isEmpty方法如果棧為空的話將返回 true,否則就返回 false。

isEmpty() { 
    return this.items.length === 0; 
}

實現size方法返回棧的長度。

size() { 
    return this.items.length; 
}
4.2.6 清空棧元素

clear 方法用來移除棧里所有的元素,把棧清空。

clear() { 
    this.items = []; 
}
4.2.7 使用 Stack 類

首先需要初始化 Stack 類,然 后驗證一下棧是否為空(輸出是 true,因為還沒有往棧里添加元素)。
接下來,往棧里添加一些元素(這里我們添加數字 5 和 8;)。

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,因為棧里有三個元素(5、8 和 11)。調用 isEmpty 方法,會看到輸出了 false。
再添加一個元素。

stack.push(15);


調用兩次 pop 方法從棧里移除兩個元素。

stack.pop(); 
stack.pop(); 
console.log(stack.size()); // 輸出 2

調用兩次后,現在棧里僅剩下 5 和 8 了。 下圖描繪了這個執行過程。

4.3 創建一個基於 JavaScript 對象的 Stack 類

O(n)的意 思是,我們需要迭代整個數組直到找到要找的那個元素,其中的 n 代表數組的長度。如果數組有更多元素的話,所需的時間會更長,會占用更多的內存空間。
怎么樣能直接獲取元素,占用較少的內存空間,並且仍然保證所有元素按照我們的需要排列?
使用一個 JavaScript 對象來存儲所有的棧元素,保證它們的順序並且遵循 LIFO 原則。
首先像下面這樣聲明一個 Stack 類(stack.js 文件)。

class Stack { 
    constructor() { 
        this.count = 0; 
        this.items = {}; 
    } 
    // 方法 
}

在這個版本的 Stack 類中,使用一個 count 屬性來幫助我們記錄棧的大小(也能幫助我們從數據結構中添加和刪除元素)。

4.3.1 向棧中插入元素

由於現在使用了一個對象,這個版本的 push 方法只允許我們一次插入一個元素。

push(element) { 
    this.items[this.count] = element; 
    this.count++; 
}

要向棧中添加元素,我們將使用 count 變量 作為 items 對象的鍵名,插入的元素則是它的值。在向棧插入元素后,我們遞增 count 變量。
延用之前的示例來使用 Stack 類,並向其中插入元素 5 和 8。

const stack = new Stack(); 
stack.push(5); 
stack.push(8);

在內部,items 包含的值和 count 屬性如下所示。

items = { 
    0: 5, 
    1: 8 
}; 
count = 2;

4.3.2 驗證一個棧是否為空和它的大小
count 屬性也表示棧的大小。因此,我們可以簡單地返回 count 屬性的值來實現 size 方法。

size() { 
    return this.count; 
}

要驗證棧是否為空,可以像下面這樣判斷 count 的值是否為 0。

isEmpty() { 
    return this.count === 0; 
}
4.3.3 從棧中彈出元素
pop() {
    if (this.isEmpty()) { // {1} 
        return undefined; 
    } 
    this.count--; // {2} 
    const result = this.items[this.count]; // {3} 
    delete this.items[this.count]; // {4} 
    return result; // {5}
}

首先,我們需要檢驗棧是否為空(行{1})。如果為空,就返回 undefined。如果棧不為空 的話,我們會將 count 屬性減 1(行{2}),並保存棧頂的值(行{3}),以便在刪除它(行{4}) 之后將它返回(行{5})。
使用如下內部的值來模擬 pop 操作。

items = { 
    0: 5,
    1: 8 
}; 
count = 2;

要訪問到棧頂的元素(即最后添加的元素 8),我們需要訪問鍵值為 1 的位置。因此我們將 count 變量從 2 減為 1。這樣就可以訪問 items[1],刪除它,並將它的值返回了。

4.3.4 查看棧頂的值並將棧清空

peek 方法(查看棧頂值) 的代碼。

peek() { 
    if (this.isEmpty()) { 
        return undefined; 
    } 
    return this.items[this.count - 1]; 
}

要清空該棧,只需要將它的值復原為構造函數中使用的值即可。

clear() { 
    this.items = {}; 
    this.count = 0; 
}

也可以遵循 LIFO 原則,使用下面的邏輯來移除棧中所有的元素。

while (!this.isEmpty()) { 
    this.pop(); 
}
4.3.5 創建 toString 方法

對於使用對象的版本,我們將創建一個 toString 方法來像數組一 樣打印出棧的內容。

toString() {
    if (this.isEmpty()) { 
        return ''; 
    }
    let objString = `${this.items[0]}`; // {1} 
    for (let i = 1; i < this.count; i++) { // {2}
        objString = `${objString},${this.items[i]}`; // {3} 
    }
    return objString;
}

如果棧是空的,我們只需返回一個空字符串即可。如果它不是空的,就需要用它底部的第一 個元素作為字符串的初始值(行{1}),然后迭代整個棧的鍵(行{2}),一直到棧頂,添加一個逗 號(,)以及下一個元素(行{3})。如果棧只包含一個元素,行{2}和行{3}的代碼將不會執行。

4.4 保護數據結構內部元素

對於 Stack 類來說,要確保元素只會被添加到棧頂,而不是棧 底或其他任意位置(比如棧的中間)。而我們在 Stack 類中聲明的 items 和 count 屬 性並沒有得到保護。

const stack = new Stack(); 
console.log(Object.getOwnPropertyNames(stack)); // {1} 
console.log(Object.keys(stack)); // {2} 
console.log(stack.items); // {3}

行{1}和行{2}的輸出結果是["count", "items"]。這表示 count 和 items 屬性是公開 的,我們可以像行{3}那樣直接訪問它們。根據這種行為,我們可以對這兩個屬性賦新的值。
本章使用 ES2015(ES6)語法創建了 Stack 類。我們希望 Stack 類的用戶只能訪問我們在類中暴露的方法。

4.4.1 下划線命名約定

下划線命名約定就是在屬性名稱之前加上一個下划線(_)來標記一個屬性為私有屬性。

class Stack { 
    constructor() { 
        this._count = 0; 
        this._items = {}; 
    } 
}
4.4.2 用 ES2015 的限定作用域 Symbol 實現類

ES2015 新增了一種叫作 Symbol 的基本類型,它是不可變的,可以用作對象的屬性。
用它在 Stack 類中聲明 items 屬性。

const _items = Symbol('stackItems'); // {1} 
class Stack { 
    constructor () {
        this[_items] = []; // {2}
    } // 棧的方法
}

在上面的代碼中聲明了 Symbol 類型的變量_items(行{1}),在類的 constructor 函數中初始化它的值(行 {2} )。 要訪問 _items , 只需要把所有的 this.items 都換成 this[_items]。
這種方法並沒有真正的實現屬性私有化, 因為 ES2015 新增的 Object.getOwnPropertySymbols 方法能夠取到類里面聲明的所有 Symbols 屬性。下面是一個破壞 Stack 類的例子。

const 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 屬性是一個數組,可以進行任意的數組操作。但我們操作的是棧,不應該出現這種行為。

4.4.3 用 ES2015 的 WeakMap 實現類

WeakMap是一種可以確保屬性是私有的數據類型。WeakMap 可以存儲鍵值對,其中鍵是對象,值可以是任意數據 類型。
如果用 WeakMap 來存儲 items 屬性(數組版本),Stack 類就是這樣的:

const items = new WeakMap(); // {1}
class Stack {
    constructor () { 
        items.set(this, []); // {2} 
    } 
    push(element){
        const s = items.get(this); // {3}
        s.push(element); 
    } 
    pop(){
        const s = items.get(this);
        const r = s.pop();
        return r; 
    } 
    // 其他方法
}

行{1},聲明一個 WeakMap 類型的變量 items。
行{2},在 constructor 中,以 this(Stack 類自己的引用)為鍵,把代表棧的數組 存入 items。
行{3},從 WeakMap 中取出值,即以 this 為鍵(行{2}設置的)從 items 中取值。
現在items 在 Stack 類里是真正的私有屬性。采用這種方法,代碼的可讀性 不強,而且在擴展該類時無法繼承私有屬性。

4.4.4 ECMAScript 類屬性提案

JavaScript 類中增加私有屬性的提案。下面是一個例子。

class Stack {
    #count = 0;
    #items = 0;
    // 棧的方法
}

通過在屬性前添加井號(#)作為前綴來聲明私有屬性。

4.5 用棧解決問題

棧的實際應用范圍。在回溯問題中,它可以存儲訪問過的任務或路徑、撤銷的操作。
從十進制到二進制
要把十進制轉化成二進制,我們可以將該十進制數除以 2(二進制是滿二進一)並對商取整, 直到結果是 0 為止。
舉個例子,把十進制的數 10 轉化成二進制的數字,過程大概是如下。

function decimalToBinary(decNumber) { 
    const remStack = new Stack(); 
    let number = decNumber; 
    let rem; 
    let binaryString = '';

    while (number > 0) { // {1} 
        rem = Math.floor(number % 2); // {2} 
        remStack.push(rem); // {3} 
        number = Math.floor(number / 2); // {4} 
    }
    while (!remStack.isEmpty()) { // {5} 
        binaryString += remStack.pop().toString(); 
    }

    return binaryString;
}

在這段代碼里,當除法的結果不為 0 時(行{1}),我們會獲得一個余數,並放到棧里(行 {2}、行{3})。然后讓結果繼續除以 2(行{4})。
JavaScript 不會區分整數和浮點數。因此,要使用 Math.floor 函數僅返回除法運算結果的整數部分。最后,用 pop 方法把棧中的元素都移除,把出棧的元素連接成字符串(行{5})。
使用以下代碼把結果輸出到控制台里。

console.log(decimalToBinary(233)); // 11101001 
console.log(decimalToBinary(10)); // 1010 
console.log(decimalToBinary(1000)); // 1111101000

修改之前的算法,使之能把十進制轉換成基數為 2~36 的任意進制。

function baseConverter(decNumber, base) {
    const remStack = new Stack(); 
    const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // {6} 
    let number = decNumber; 
    let rem; 
    let baseString = '';
    if (!(base >= 2 && base <= 36)) { 
        return ''; 
    }
    while (number > 0) { 
        rem = Math.floor(number % base); 
        remStack.push(rem); 
        number = Math.floor(number / base); 
    }
    while (!remStack.isEmpty()) { 
        baseString += digits[remStack.pop()]; // {7} 
    }
    return baseString;
}

在將十進制轉成二進制時,余數是 0 或 1;在將十進制轉成八進 制時,余數是 0~7;但是將十進制轉成十六進制時,余數是 0~9 加上 A、B、C、D、E 和 F(對 應 10、11、12、13、14 和 15)。因此,我們需要對棧中的數字做個轉化才可以(行{6}和行{7})。 因此,從十一進制開始,字母表中的每個字母將表示相應的基數。字母 A 代表基數 11,B 代表 基數 12,以此類推。
可以使用之前的算法,輸出結果如下。

console.log(baseConverter(100345, 2)); // 11000011111111001 
console.log(baseConverter(100345, 8)); // 303771 
console.log(baseConverter(100345, 16)); // 187F9 
console.log(baseConverter(100345, 35)); // 2BW0


免責聲明!

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



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