JS的数据结构-栈

95 阅读5分钟

前言

作为一名合格的码农,算法始终是绕不开的一座大山,即使作为前端程序员也需要学习好算法与数据结构的相关知识。本文旨在深入浅出地解析JavaScript中的栈这一数据结构,从概念出发,逐步探索其工作原理,再到实际应用,通过代码方式来建立自己的JavaScript数据结构与算法库。

栈的基本概念

在数据结构中,栈(Stack)是一种非常基本且重要的线性数据结构。它的主要特性是后进先出(Last In, First Out,简称LIFO)。这意味着最后一个进入栈的元素将是第一个被移除的元素。这种数据结构的典型例子可以是盘子里叠放的盘子,你总是从顶部拿走或添加盘子。 以下是栈的一些关键概念和术语:

  1. 栈顶(Top)

    • 栈顶是栈中允许进行插入和删除操作的一端。在任何给定时刻,栈顶元素是最近被添加到栈中的元素,也是唯一可以被访问或移除的元素。
  2. 栈底(Bottom)

    • 栈底是栈的另一端,通常是固定的,不允许进行插入或删除操作。在空栈的情况下,栈顶和栈底实际上是指同一位置。
  3. 空栈

    • 当栈中没有任何元素时,称其为空栈。此时,栈顶和栈底都指向同一个位置,通常这个位置是栈的初始位置或者说是基地址。
  4. 入栈(Push)

    • 入栈操作是在栈顶插入一个新的元素。当执行入栈操作时,新元素成为新的栈顶元素。
  5. 出栈(Pop)

    • 出栈操作是从栈顶移除并返回栈顶元素。一旦执行出栈操作,原栈顶元素被移除,下一个元素(如果存在)成为新的栈顶元素。
  6. 读栈顶元素(Peek/Top)

    • 读栈顶元素操作用于查看当前栈顶元素而不将其移除。这有助于在不改变栈状态的情况下获取栈顶元素的信息。

创建一个基于数组的栈

在JavaScript中,数组的操作与结构是最接近栈的概念的。我们要做的就是通过数组来模拟入栈、出栈等操作。

class starkArr {
    constructor() {
        this.arr = [];
    }

    // 添加元素
    push(element) {
        this.arr.push(element);
    }

    // 删除元素
    pop() {
        return this.arr.pop();
    }
    //查看栈顶元素
    peek() {
        return this.arr[this.arr.length - 1];
    }

    // 判断数组是否为空
    isEmpty() {
        return this.arr.length === 0;
    }

    // 获取数组长度
    size() {
        return this.arr.length;
    }

    // 清空数组
    clear() {
        this.arr.length = 0;
    }
}

创建一个基于对象结构的栈

以上我们通过数组形式来创建了一个栈的类,但是我们在处理大量数据等时候,我们同样需要评估如何操作数据是最高效的,当我们在使用数组的时候,大部分的方法的时间复杂度是O(n),而且,数组是元素的一个有序集合,为了保证元素排列有序,它会占用更多的内存空间。我们可以利用对象的形式来创建一个模拟栈的类,来解决上述问题。

class starkObj {
    constructor() {
        this.obj = {};
        this.count = 0;
    }

    // 添加元素
    push(element) {
        this.obj[this.count++] = element;
    }

    // 删除元素
    pop() {
        if (this.count === 0) {
            return undefined;
        }
        this.count--;
        const lastElement = this.obj[this.count];
        delete this.obj[this.count];
        return lastElement;
    }

    peek() {
        if(this.count === 0) return undefined;
        return this.obj[this.count - 1];
    }

    // 判断数组是否为空
    isEmpty() {
        return this.count === 0;
    }

    // 获取数组长度
    size() {
        return this.count;
    }

    // 清空数组
    clear() {
        this.obj = {};
        this.count = 0;
    }

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

上面我们所创建的starkObj类,除了toString方法,其他方法的复杂度均为O(1),代表我们可以直接找到目标元素并对其进行操作。

保护数据结构内部元素

当我们创建了一个公共方法或者类时,我们希望保护内部所创建的变量和元素,只用通过我们内部暴露的方法可以修改内部的结构和变量。上面所创建的starkObj类,其内部的obj和count变量都是公开的,意味着我们可以直接操作它们。所以我们需要通过一种方式来将其变成内部变量。

通过WeakMap来实现

const starkSet = new WeakMap();
class startWeakMap {
    constructor() {
        starkSet.set(this, {
            obj: {},
            count: 0
        });
    }

    // 添加元素
    push(element) {
        const stark = starkSet.get(this);
        stark.obj[stark.count++] = element;
    }

    // 删除元素
    pop() {
        const stark = starkSet.get(this);
        if (stark.count === 0) {
            return undefined;
        }
        const lastElement = stark.obj[stark.count - 1];
        delete stark.obj[stark.count - 1];
        stark.count--;
        return lastElement;
    }

    peek() {
        const stark = starkSet.get(this);
        if (stark.count === 0) {
            return undefined;
        }
        return stark.obj[stark.count - 1];
    }

    // 判断数组是否为空
    isEmpty() {
        const stark = starkSet.get(this);
        return stark.count === 0;
    }

    // 获取数组长度
    size() {
        const stark = starkSet.get(this);
        return stark.count;
    }

    // 清空数组
    clear() {
        const stark = starkSet.get(this);
        stark.obj = {};
        stark.count = 0;
    }

    toString() {
        const stark = starkSet.get(this);
        if (stark.count === 0) {
            return '';
        }
        let objString = `${stark.obj[0]}`;
        for (let i = 1; i < stark.count; i++) {
            objString = `${objString},${stark.obj[i]}`;
        }
        return objString;
    }
}

通过WeakMap来确保我们所创建的变量和属性都为私有的。当然在TS中,提供了一个给类属性和方法使用的private修饰符,也能来声明私有变量。

用栈的形式来解决问题

我们可以通过使用栈的形式来实现一个进制转换算法

// 功能:实现进制转换
// 参数:decNumber:十进制数;base:进制
// 返回值:转换后的字符串
function baseConversion(decNumber, base) {
    // 将参数转换为数字
    decNumber = Number(decNumber);
    base = Number(base);
    // 如果参数不是数字或者数字不是整数,或者进制小于2或者大于36,返回空字符串
    if (isNaN(decNumber) || isNaN(base) || !Number.isInteger(decNumber) || decNumber < 0 || base < 2 || base > 36) {
        return '';
    }
    // 创建一个栈
    const stack = new startWeakMap();
    // 进制转换的字符串
    let rem,
        baseString = '',
        // 定义0-9, A-Z的字符串
        digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';  
    // 当十进制数大于0时,进行循环
    while (decNumber > 0) {
        // 取余数
        rem = decNumber % base;
        // 将余数压入栈中
        stack.push(rem);
        // 取整
        decNumber = Math.floor(decNumber / base);
    }
    // 当栈不为空时,将栈中的数弹出,并添加到字符串中
    while (!stack.isEmpty()) {
        baseString += digits[stack.pop()];
    }
    // 返回转换后的字符串
    return baseString;
}

总结

在本篇文章中,我们不仅揭开了JavaScript中栈这一数据结构的神秘面纱,还深入理解了其背后的逻辑与应用。栈,作为一种遵循后进先出(LIFO)原则的线性数据结构,以其简单而强大的特性,在解决编程中的诸多问题时发挥着不可替代的作用。

从算法设计到编译器原理,从函数调用栈到浏览器的历史记录管理,栈的身影无处不在。掌握了栈,就意味着我们获得了一把开启更高级算法和数据处理技术大门的钥匙。

我们学习了如何在JS中手动实现一个栈,这不仅仅是对语言特性的熟练运用,更是一种思维的锻炼——学会抽象复杂问题,将其分解为可管理的部分,再用合适的工具去解决。这种能力,对于每一位程序员而言,都是极其宝贵的财富。

本文是作者在自己学习《学习JavaScript数据结构与算法》这本书时随便记录一下, 若有错误请指正,多多包涵。感谢支持!