JavaScript 数据结构和算法——栈

362 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 1 天,点击查看活动详情

介绍

数据结构在编程当中占据非常重要的作用。数据结构 + 算法 = 程序这个公式在计算机科学的地位丝毫不亚于爱因斯坦的相对论在物理学当中的地位。作为程序的重要组成部分,数据结构究竟有什么样的魅力呢?

数据结构是相互之间存在一种或多种特定关系的数据元素的集合。换句话说,数据结构是带结构的数据元素的集合,结构就是指数据元素之间存在的关系。 数据结构包括逻辑结构存储结构两个层次。

  • 逻辑结构
    • 数据的逻辑结构是从逻辑关系上描述数据,它与数据的存储无关,是独立于计算机的。
  • 存储结构
    • 集合结构
    • 线性结构
    • 树结构
    • 图形结构或网状结构

其中集合结构、树结构、图结构都属于非线性结构。 线性结构包括线性表(典型的线性结构)、栈和队列(具有特殊限制的线性表,数据操作只能在表的一段或两端进行)、字符串、数组。 非线性结构包括树、二叉树、有向图、无向图。 数据的逻辑结构

思想

本篇文章我们来介绍数据结构当中的线性结构——栈。栈的基本思想是先进后出。你可以理解为一叠餐盘,最早放的往往是最后一个取的。并且,栈不允许从中间对栈当中的元素进行操作,只能通过栈顶来获取和删除元素。这样做的好处是可以控制控制栈当中元素的操作顺序。栈也被用在编程语言的编译器和内存中保存变量、方法的调用,也被用于浏览器历史记录。下面我们来实现一栈。

实现方式

数组方式

//使用数组来构建栈

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

  //添加栈顶元素
  push(element) {
    this.items.push(element);
  }
  // 删除栈顶元素
  pop() {
    let result = this.items.pop();
    return result;
  }
  //返回栈顶元素
  peek() {
    return this.items[this.items.length - 1];
  }
  // 判断栈是否为空
  isEmpty() {
    return this.items.length === 0;
  }

  //获取栈长度
  size() {
    return this.items.length;
  }
}

// 创建栈
const stack = new Stack();

JavaScript 对象方式

//使用对象来构建栈

class Stack {
  constructor() {
    this.itemObj = {};
    this.size = 0;
  }

  //增加元素
  push(item) {
    this.itemObj[this.size] = item;
    this.size++;
  }

  //删除元素
  pop() {
    //判断当前栈是否为空
    if (this.isEmpty) return;

    const result = this.itemObj[this.size - 1];
    delete this.itemObj[this.size - 1];
    this.size--;
  }

  //判断栈是否为空
  isEmpty() {
    return this.size === 0;
  }

  //返回栈顶元素
  peek() {
    return this.itemObj[this.size - 1];
  }

  //返回栈长度
  length() {
    return this.size;
  }
}

//创建栈
const stack = new Stack();

保护数据结构内部元素

在创建公共的数据结构的时候,我们希望可以保护类当中的私有属性,但是 JavaScript 类实现其实是依赖于原型链来实现的,在类当中定义的属性,都可以在原型链当中被找到,者显然是不安全的。也就是我们可以绕过我们暴露出去的方法,而通过原型链来直接修改我们内部的this.items数组,这样显然是违背我们的意愿的,因为我们需要对this.items数组进行限制操作,他只能通过我们暴露出去的方法来进行操作。接下来我们来看下其他 JavaScript 实现私有属性的方法。

  • 下划线命名约定 在开发当中,我们一般使用_命名来约定这是一个私有属性
    class Stack {
      constructor() {
        //约定为私有属性
        this._items = [];
      }
    }
    
    但是这样的约定没有强制性,如果别人不遵守约定,那么和没有也没区别了,防君子不防小人。(我这个君子也防 T_T)。
  • 用 Symbol 实现类 Symbol 是 JavaScriptES2015 新增加的一个基本类型。它是不可变的,可以用作对象的属性。并且该属性无法被Object.getOwnPropertyNames()获取到。
//使用Symbol来实现
const _items = Symbol("stackItem");

class Stack {
  constructor() {
    this[_items] = []; //为Symbol类型赋值数组
  }
  //增加元素
  push(item) {
    this[_items].push(item);
  }

  pop() {
    let result = this[_items].pop();
    return result;
  }
  //返回栈顶元素
  peek() {
    return this[_items][this[_items].length - 1];
  }
  // 判断栈是否为空
  isEmpty() {
    return this[_items].length === 0;
  }

  //获取栈长度
  size() {
    return this[_items].length;
  }
}

使用上面的方法虽然不能通过原型链找到this.items属性,但是,JavaScript 还是提供了一个 APIObject.getOwnPropertySymbols 来获取对象当中的Symbol属性。

const objectSymbol = Object.getOwnPropertySymbols(stack);
console.log(objectSymbol); //[ Symbol(stackItem) ]
stack[objectSymbol[0]].push(0);
console.log(stack.size()); //1
console.log(stack.peek()); //0
  • 用 weekMap 实现类
//创建weekMap存储
const items = new WeekMap();
console.log(items);
class WeekStack {
  constructor() {
    //设置this为键,值为数组
    items.set(this, []);
  }

  //增加元素
  push(item) {
    //通过get获取指定的值,然后进行修改
    items.get(this).push(item);
  }

  //删除元素
  pop() {
    //通过get获取指定的值,然后进行修改
    return items.get(this).pop();
  }
}

这样虽然增加了私有属性,但是耦合性太高了,并且在扩展类的时候无法进行继承。