JavaScript数据结构 - 栈

1,483 阅读8分钟

大家好,我是前端图图。今天就来聊聊的数据结构,因为最近在学数据结构和算法,我也把写文章当作一次复盘。下面废话不多说,开始吧!

栈的数据结构

栈是一种后进先出原则的有序集合(就是后面进去的先出来的意思)。添加或待删除的元素都保存在栈的同一端,叫作栈顶(栈的末尾),另一端叫作栈底。在栈里面,新元素都在栈顶,旧的元素都在栈底。

在生活中有许多用来描述栈的例子。例如:叠放的书籍、盘子。

在计算机中,栈被用在编译器和内存中保存变量、方法调用等等,也被用在浏览器的历史记录(浏览器的返回按钮)。

基于数组的栈

下面用ES6的类来创建一个栈。

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

对于栈来说,可以使用数组,也可以使用对象。只要遵从LIFO(后进先出)原则就行。

下面是栈的一些方法。

  • push(ele):添加一个或多个元素到栈顶。
  • pop():移除栈顶的元素并返回移除的元素。
  • peek():获取栈顶的元素。
  • isEmpty():校验是否为空栈,如果为空就返回true,否则返回false
  • clear():清空栈。
  • size():返回栈中的元素数量。

向栈添加元素

push方法负责向栈内添加元素,这个方法只添加元素到栈顶(也就是栈的末尾)。

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

这里是数组来保存栈的元素,所以直接使用数组的push方法。

从栈里移除元素

pop方法负责移除栈里的元素。栈遵从LIFO原则,所以移除最后添加的元素。

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

pushpop这两个方法操作栈里的元素,这样自然遵从LIFO原则了。

查看栈顶元素

如果想知道栈里最后添加的元素是什么,用peek方法即可。

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

length - 1即可访问数组最后的一个元素。

上面的图中,数组中包含了三个元素1, 2, 3,数组的长度是3。而length - 1(3-1)就是2

检查栈是否为空

isEmpty方法校验栈是否为空栈,为空就返回true,否则返回false

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

这里简单的判断一下数组的长度是否为0就可以了。

获取栈里的元素个数

size方法返回数组的长度就可以获取元素的个数了。

size() {
  return this.items.length;
}

清空栈

clear方法移除栈中的所有元素,最简单的方式就是把items初始化成一个空数组。

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

这样就完成了栈的方法。

使用 Stack 类

首先初始化一个Stack类,然后查看栈是否为空。

const stack = new Stack();
console.log(stack.isEmpty()); // true 为true就代表栈是空的

然后,向栈里添加元素。

stack.push(1);
stack.push(2);

console.log(stack.peek()); // 2

这里添加了12,当然你可以添加任何类型的元素。然后调用了peek方法,输出的是2,因为它是栈里最后一个元素。

再往栈里添加一个元素。

stack.push(10);
console.log(stack.size()); // 3
console.log(stack.isEmpty()); // false

我们往栈里添加了10。调用size方法,输出的是3,栈里有三个元素。调用isEmpty方法,输出的是false

下面展示了到现在为止对栈的操作,以及栈的当前状态。

在调用pop方法之前,栈里有三个元素,调用两次后,现在栈只剩下1了。

stack.pop();
stack.pop();
console.log(stack.size()); // 1

整体代码

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

  push(ele) {
    this.items.push(ele);
  }
  pop() {
    return this.items.pop();
  }
  peek() {
    return this.items[this.items.length - 1];
  }
  isEmpty() {
    return this.items.length === 0;
  }
  size() {
    return this.items.length;
  }
  clear() {
    this.items = [];
  }
}

const stack = new Stack();
console.log(stack.isEmpty()); // true

stack.push(1); // 向栈里添加了元素1
stack.push(2); // 向栈里添加了元素2

console.log(stack.peek()); // 此时栈里最后一个元素为2

stack.push(10); // 又往栈里添加一个元素10
console.log(stack.size()); // 这时栈的长度就变成了3
console.log(stack.isEmpty()); // false

stack.pop(); // 从栈顶中移除了一项
stack.pop(); // 从栈顶中又移除了一项
console.log(stack.size()); // 从栈中移除了两个元素,最后获取栈的长度就是1

基于对象的栈

一样的,使用类来创建基于对象的栈。

class Stack{
  constructor {
    this.count = 0;
    this.items = {};
  }
}

对象版本的Stack类中,用count属性来记录栈的大小(也可以从栈中添加和删除元素)。

往栈中插入元素

在数组的版本中,可以向Stack类中添加多个元素,而在对象这个版本的push方法只允许一次插入一个元素。

push(ele) {
  this.items[this.count] = ele;
  this.count++;
}

在JavaScript中,对象是一系列键值对的集合。要向栈中添加元素,使用count变量作为items对象的键名,插入的元素就是它的值。向栈插入元素之后,就递增count变量。

const stack = new Stack();
stack.push(5);
stack.push(10);
console.log(stack);
// {count: 2, items: {0: 5, 1: 10}}

可以看到Stack类内部items里的值和count属性的值在最后的log中输出。

验证栈是否为空和它的大小

count属性也表示栈的大小。这样就可以用count属性的值来实现size方法。

size() {
  return this.count;
}

判断栈是否为空的话,验证count的值是否为0就可以了。

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

从栈中移除元素

由于没有使用数组来存储元素,就要手动实现移除元素的逻辑。pop方法一样返回从栈移除的元素。

pop() {
  // 首先判断栈是否为空,如果为空,就返回undefined
  if (this.isEmpty()) {
    return undefined;
  }
  // 如果栈不为空的话,就将`count`属性减1
  this.count--;
  // result保存了栈顶的元素
  const result = this.items[this.count];
  // 删除栈顶的元素
  delete this.items[this.count];
  // 之后返回刚才保存的栈顶元素
  return result;
}

获取栈顶的元素

访问栈顶的元素,只需要把count属性减1即可。

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

清空栈

清空栈只需要把它的值设置成初始化时的值就行了。

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

当然还可以用下面这种方法移除栈里的所有元素。

anotherClear() {
  while(!this.isEmpty()) {
  	this.pop();
  }
}

创建toString方法

在数组的版本中,并不需要创建toString方法,因为可以使用数组的toString方法。但对象的版本,就要创建一个toString方法来像数组那样输出栈的内容。

toString() {
  // 栈为空,将返回一个空字符串。
  if (this.isEmpty()) {
    return "";
  }

  // 栈不为空,就需要用它底部的第一个元素作为字符串的初始值
  let objString = `${this.items[0]}`;
  // 栈只包含一个元素,就不会执行`for`循环。
  for (let i = 1; i < this.count; i++) {
    // 迭代整个栈的键,一直到栈顶,添加一个逗号(,)以及下一个元素。
    objString = `${objString},${this.items[i]}`;
  }
  return objString;
}

这样就完成了两个不同版本的Stack类。这也是一个不同方法写代码的例子。对于使用Stack类,选择使用数组还是对象并不重要,两种方法都提供一样的方法,只是内部实现不一样而已。

整体代码

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

  push(ele) {
    this.items[this.count] = ele;
    this.count++;
  }

  size() {
    return this.count;
  }

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

  pop() {
    // 首先判断栈是否为空,如果为空,就返回undefined
    if (this.isEmpty()) {
      return undefined;
    }
    // 如果栈不为空的话,就将`count`属性减1
    this.count--;
    // result保存了栈顶的元素
    const result = this.items[this.count];
    // 这里是删除栈顶的元素,由于使用的是对象,所以可以使用delete运算符从对象中删除一个特定的值
    delete this.items[this.count];
    // 之后返回栈顶的元素
    return result;
  }

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

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

  anotherClear() {
    while (!this.isEmpty()) {
      this.pop();
    }
  }

  toString() {
    // 栈为空,将返回一个空字符串。
    if (this.isEmpty()) {
      return "";
    }

    // 栈不为空,就需要用它底部的第一个元素作为字符串的初始值
    let objString = `${this.items[0]}`;
    // 栈只包含一个元素,就不会执行`for`循环。
    for (let i = 1; i < this.count; i++) {
      // 迭代整个栈的键,一直到栈顶,添加一个逗号(,)以及下一个元素。
      objString = `${objString},${this.items[i]}`;
    }
    return objString;
  }
}

用栈解决问题

栈的实际应用非常广泛。它可以存储访问过的任何或路径、撤销的操作。

从十进制到二进制

在生活中,我们主要使用十进制。但在计算机中,二进制非常重要,因为计算机的所有内容都是用二进制数字表示的(01)。

要把十进制转成二进制,可以将该十进制数除以2(二进制是满二进一)并对商取整,直到结果是0为止。举个例子,把十进制的数10转成二进制的数字,下面是对应的算法。

function decimal(num) {
  const remStack = []; // 存储二进制的栈
  let number = num; // 需要转成二进制的数
  let rem = ""; // 余数
  let binaryString = ""; // 存储推出栈的元素

  // 当参数不为0时,进入while语句
  while (number > 0) {
    rem = Math.floor(number % 2);
    remStack.push(rem); // 把余数添加到remStack数组中
    number = Math.floor(number / 2); // number除以2,得到下次要取余数的值,此时的number的值已经不是传入的参数了。
  }

  while (remStack.length !== 0) {
    // 用pop方法把栈中的元素移除,将移除栈的元素连成字符串
    binaryString += remStack.pop().toString();
  }
  return binaryString;
}

console.log(decimal(10)); // 1010
console.log(decimal(100)); // 1100100

在上面这段代码里,当参数不是0时,进入while语句。就得到一个余数赋值给rem,并放入栈里。然后让number除以2,就得到了下次进入while语句取余数的值。要注意的是,此时的number的值已经不是传入参数的值了。最后,用pop方法把栈中的元素移除,将移除的元素连成字符串。

进制转换算法

修改之前的算法,可以将十进制转成计数为2~36的任何进制。除了把十进制除以2转成二进制数外,还可以传入其他任何禁止的基数为参数。

function baseConverter(num, base) {
  const remStack = [];
  const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  let number = num;
  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.length !== 0) {
    baseString += digits[remStack.pop()];
  }
  return baseString;
}

console.log(baseConverter(10000, 2)); // 10011100010000
console.log(baseConverter(10000, 8)); // 23420
console.log(baseConverter(10000, 16)); // 64
console.log(baseConverter(10000, 36)); // 7PS

上面的代码只需要改一个地方,把十进制转成二进制的时候,余数是01,再把十进制转八进制的时候,余数是0~7;但是把十进制转十六进制时,余数是0~9再加上A、B、C、D、E、F(对应 10、11、12、13、14、15)。所以需要对栈中的数组做一个转换才行(baseString += digits[remStack.pop()]这段代码)。从十一进制开始,字母表中的每个字母都对应一个基数,A就代表基数11B就代表基数12,以此类推。

总结

个人感觉操作栈数据结构相对于其他的数据结构来说,还是比较简单的。不管用对象还是数组都可以实现栈数据结构,只要遵从LIFO(后进先出)原则即可。喜欢的掘友可以点击关注+点赞哦!后面会持续更新其他数据结构,也把自己学的知识分享给大家。当然写作也可以当成复盘。2021年加油!实现自己的目标。