学习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

posted on 2020-04-12 21:48  donokamark  阅读(1182)  评论(0编辑  收藏  举报