【2】js栈的实现-JavaScript学习数据结构

152 阅读7分钟

栈的概念

维基百科:堆栈(英语:stack)又称为堆叠,是计算机科学中的一种抽象资料类型,只允许在有序的线性资料集合的一端(称为堆栈顶端,top)进行加入数据(push)和移除数据(pop)的运算。因而按照后进先出(LIFO, Last In First Out)的原理运作。

栈如一叠盘子,我们只能在上面添加盘子,每次也只能取走最上面的一个盘子。所以任何不在栈的元素都是无法访问的,为了得到栈底的元 素,必须先拿掉上面的元素。

栈的示意图

基本操作

操作名称定义方法目的说明
入栈push()将一个元素压入栈顶
出栈pop()将一个元素从栈顶弹出
访问栈顶peek()查看栈顶的元素
清空clear()清空栈内的所有元素
是否为空isEmpty()检查栈内是否还有元素

用数组实现栈

创建栈

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

    push(element) {
        this.items.push(element);
    };

    pop() {
        return this.items.pop()
    };

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

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

    clear() {
        this.items = [];
    };
}

使用栈

let stack = new Stack();

console.log(s.isEmpty) //true
实例化一个栈stack,此时栈内为空

往空栈里面添加元素、移除元素

//添加元素,一次加一个(叠盘子一样)
stack.push(5);
stack.push(8);
stack.push(11);
stack.push(15);

现在栈内元素为:5,8,11,15 最后加入的15在栈顶

具体console.log(stack);
stack: {
 items: [5, 8, 11, 15]
}
栈元素,存在对象stack的items属性(数组)里

//移除元素,从栈顶开始,每次弹走一个
stack.pop(15);
stack.pop(11);

用类实现栈

创建一个 Stack 类最简单的方式是使用一个数组来存储其元素。在处理大量数据的时候(这在现实生活中的项目里很常见),我们同样需要评估如何操作数据是最高效的。在使用数组时, 大部分方法的时间复杂度是 O(n)。除了用数组,在JavaScript中,也可以通过对象来储存所有的栈元素,只要保证它们的顺序遵循LIFO规则。

class Stack {
    constructor() {
        this.counter = 0;
        this.items = {};
    }

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

    isEmpty() {
        return this.counter === 0 ? true : false;
    }

    pop() {
        if (this.isEmpty()) {
            return undefined;
        } else {
            this.counter--;
            let deletedEle = this.items[this.counter];
            delete this.items[this.counter];
            return deletedEle;
        }
    }

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

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

对象与数组实现数组的方式是类似的,其实数组只是特殊对象,其键值对中属性名为索引而已。在用对象实现栈时,我们要知道,对象访问数组处理用obj.property这种形式,也可以ojb[property],并且中括号里的属性名可以是变化的。

let person = {
    name: "Alice",
    age: 20
}

//对象访问属性,两种方式相同
console.log(person.name)
console.log(person["name"])

在创建别的开发者也可以使用的数据结构或对象时,我们希望保护内部的元素,只有我们暴露出的方法才能修改内部结构。对于 Stack 类来说,要确保元素只会被添加到栈顶,而不是栈 底或其他任意位置(比如栈的中间)。不幸的是,我们在 Stack 类中声明的 items 和 count 属性并没有得到保护(数据保护),因为 JavaScript 的类就是这样工作的。

let detail = stack.items;
//我们是能直接访问储存栈的items对象,并且可以直接对其进行修改,如下:
items.items[0] = 'hello';  
//这里我们直接把栈里的第一个元素(位于栈底)修改成了“hello”字符,安装栈的定义,我们只能访问栈定的那一个元素,像这样的操作是不可以的,应该都是封装起来,用户接触不到才行!

ES6中的类是基于原型的,尽管基于原型 的类能节省内存空间并在扩展方面优于基于函数的类,但这种方式不能声明私有属性(变量)或 方法。另外,我们希望 Stack 类的用户只能访问我们在类中暴露的方法。下面来看 看其他使用 JavaScript 来实现私有属性的方法

JavaScript保护数据结构内部元素

上面的数组与对象实现栈的方式,虽然都成功实现了栈ADT的基本操作。但是严格来说,都还不是真正意义上的栈,因为它们都还没有完全封装好。要想起到保护数据的作用,那先要知道JavaScript中实现似有属性的方法。

下划线命名约定

一部分开发者喜欢在 JavaScript 中使用下划线命名约定来标记一个属性为私有属性。但是这仅仅是一种大家的约定而已,本质上没有没有任何保护数据的作用,用户依然还是可以直接访问到栈内的数据。

class Stack { 
 constructor() { 
 this. _count = 0; 
 this. _items = {}; 
 } 
}

Symbol数据类型实现类

ES6中Symbol是一个原始数据类型,每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。更多的可以参考MDN中关于symbol介绍

//声明Symbol类型的变量_items,这个_items是唯一的
const _items = Symbol('stackItems'); 

class Stack { 
 constructor () { 
 this[_items] = []; // {2} 
 } 
 // 栈的方法
 //this.items 都换成
this[_items]
}

这种方法创建了一个假的私有属性,因为 ES2015 新增的Object.getOwnProperSymbol()方法能够取到类里面声明的所有 Symbols 属性。

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 属性是一个数组,可以进行任意的数组操作,比如从中间删除或添加元素(使用对象进 行存储也是一样的)。但我们操作的是栈,不应该出现这种行为。

Symbol能够实现假的似有属性,虽然还不能说完全的封装好,但是要想用户要想访问栈里的数据也变复杂了。

WeakMap实现类

JavaScript有一种数据类型可以确保属性是私有的,这就是WeakMapWeakMap可以存储键值对,其中键是对象,值可以是任意数据类型。

//声明一个WeakMap类型的变量items
const items = new WeakMap();

class Stack {
    constructor() {

        //在构造函数中,以this(Stack类自己的引用)为键,把代表栈的数组存入items
        items.set(this, []);
    }

    push(element) {

        //从WeakMap中取出值,以this为键,从items中取出
        const s = items.get(this);
        s.push(element);
    }

    pop() {
        const s = items.get(this);
        s.pop();
        return s[s.length - 1];
    }

    peek() {
        const s = items.get(this);
        return s[s.length - 1]
    }

    clear() {
        items.get(this) = [];
    }

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

现在我们只能使用栈暴露出来的这些操作,而无法访问到保存栈数据的items数组了。items 在 Stack 类里是真正的私有属性。采用这种方法,代码的可读性不强,而且在扩展该类时无法继承私有属性。鱼和熊掌不可兼得!

//真正满足栈的ADT定义,无法访问里面的数据
const stack = newStack();
console.log(stack.items);  //undefined
stack.push()等正常使用。

扩展:TypeScript 提供了一个给类属性和方法使用的 private 修饰符。然而,该修饰符只在编译时有用。在代码被转移完成后,属性同样是公开的。事实上,JavaScript不能像在其他编程语言中一样声明私有属性和方法。虽然有很多种方法都可以 达到相同的效果,但无论是在语法还是性能层面,这些方法都有各自的优点和缺点,具体哪种方式好,取决于自己的需求。

JavaScript的一个提案,具体没研究

栈使用例子

栈的应用很多,这里以一个简单的例子说明:如何将10进制数转换成2进制的2数。

进制转换算法说明图解

//创建一个栈的类
class Stack {
    constructor() {
        this.items = [];
    }

    push(ele) {
        this.items.push(ele);
    }

    pop() {
        this.items.pop();
    }

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

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

//具体的函数
const tenTotwo = (number) => {
    let stack = new Stack();
    let result = '';

    while (number > 0) {
        stack.push(number % 2);
        number = Math.floor(number / 2);
    }

    while (!stack.isEmpty()) {
        result += stack.peek();
        stack.pop()
    }
    return result;
}

//使用说明:将十进制的10转换成二进制。10 ——> 1010
let answer = tenTotwo(10); //1010

参考资料