实现JavaScript基本数据结构系列---栈

218 阅读3分钟

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

栈是一种后进先出(LIFO)原则的有序集合,是一种线性结构。新添加和待删除的元素都保存在栈的一端,叫做栈顶,另一端叫做栈底。最常见的的例子就是叠放的书籍等,也被用于浏览器的历史记录功能。

功能

栈是一种基本的数据结构,在日常工作中经常使用,我们可以来思考一下如何来创建一个栈呢,一个栈到底有哪些功能呢?实现功能如下:

  1. push(ele(s)): 添加一个或多个新元素到栈顶
  2. pop(): 移除栈顶元素,并返回被移除元素
  3. peek(): 返回栈顶元素,不对栈做任何修改
  4. isEmpty(): 栈是否为空
  5. clear(): 移除栈里所有元素
  6. size(): 返回栈中元素个数

实现

看到上面pushpop两个方法,大家是不是就觉得很像我们的数组呢?那我们来如何实现它呢,下面介绍了两种方法,实现起来非常简单。

基于数组的方式

数组创建一个Stack类是最简单的,相关代码也比较简单,可以直接阅读代码

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

    push(...eles) {
        this._value.push(...eles)
    }

    pop() {
        return this._value.pop()
    }

    peek() {
        return this._value[this.size - 1]
    }

    isEmpty() {
        return this.size() === 0;
    }

    clear() {
        this._value = [];
    }
    
    size() {
        return this._value.length;
    }
}

const stack = new Stack()
stack.push(1,2,3)
console.log(stack); // [1, 2, 3]

基于JavaScript对象的方式

上面第一种方式是以数组来创建一个Stack类的,他是最简单的方式来存储其元素,而我们为什么又要使用对象来实现Stack类呢?当数据量过大时,我们要考虑如何操作数据是最高效的。在使用数组时,大部分的方法都是O(n),数据量大的时候所需时间较长。我们如果能够直接获取元素,占用空间较少并且能到保证所有元素按照我们的需要排列不更好嘛?是的,所以我们使用对象来实现这一功能,可是我们知道对象的属性是无序的,那怎么让他们有序呢?下面使用了this._count = 0 来约束他, this._count依次递增,保证元素有素,从而遵循LIFO的原则。

class ObjectStack {
    constructor() {
        this._value = {};
        this._count = 0;
    }

    push(...eles) {
        eles.forEach(ele => {
            this._value[this._count] = ele;
            this._count++;
        })
    }

    pop() {
        if (this.isEmpty()) return undefined;
        this._count--;
        const result = this._value[this._count];
        delete this._value[this._count];
        return result;
    }

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

    isEmpty() {
        return this._count === 0;
    }

    clear() {
        this._value = {};
        this._count = 0;
    }
    
    size() {
        return this.__count;
    }

    toString() {
        if (this.isEmpty()) return '';
        let str = `${this._value[0]}`;
        for(let i = 1; i < this._count; i++) {
            str = `${str},${this._value[i]}`;
        }
        return str;
    }
}

const objectStack = new ObjectStack()
objectStack.push(1,2,3)
console.log(objectStack); // { '0': 1, '1': 2, '2': 3 }

除了以上方法,还有其他什么方法实现呢?我们是不是可以考虑SymbolWeakMap呢,为什么会提到这,因为如今这两种方式中this._valuethis._count是不受保护的,用户可以直接获取到然后进行修改赋值,不能确保新添加的元素只会添加到栈顶,还可能被添加到其他地方,这是不安全的,所以我们可以修改实现方式让他更加完善,大家可以尝试一下。

应用

栈的应用十分广泛,在这我们以进制转换为例进行讲解。最常见的就是十进制转二进制,那这是如何实现的呢?先来分析一下思路:

  1. 将待转换的数字除以进制数,结果不为0时,得到余数,将余数传入栈中
  2. 将待转换的数字除以进制数取整后循环第一步,直至结果为0
  3. 将栈中数字一次取出就可以组成转换后的数字

具体实现如下:

/**
 * 
 * @param {*} descNumber 要转换的数字
 * @param {*} base 进制
 * @returns 转换后的数字
 */
const baseConverter = (descNumber, base) => {
    const stack = new Stack();
    const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    let number = descNumber;
    let baseStr = '';

    if (!(base >= 2  && base <= 36)) return '';

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

    while(!stack.isEmpty()) {
        baseStr = baseStr + digits[stack._value.pop()]
    }

    return baseStr
}

console.log(baseConverter(10, 2)); // 1010
console.log(baseConverter(10, 16)); // A