JavaScript数据结构 - 栈

你们好,我是前端图图。今天就来聊聊的数据结构,由于最近在学数据结构和算法,我也把写文章看成一次复盘。下面废话很少说,开始吧!前端

栈的数据结构

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

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

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

基于数组的栈

下面用ES6的类来建立一个栈。浏览器

class Stack {
  constructor() {
    this.items = [];
  }
}
复制代码

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

下面是栈的一些方法。数据结构

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

向栈添加元素

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

push(ele) {
  this.items.push(ele);
}
复制代码

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

从栈里移除元素

pop方法负责移除栈里的元素。栈听从LIFO原则,因此移除最后添加的元素。this

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(对应 十、十一、十二、1三、1四、15)。因此须要对栈中的数组作一个转换才行(baseString += digits[remStack.pop()]这段代码)。从十一进制开始,字母表中的每一个字母都对应一个基数,A就表明基数11B就表明基数12,以此类推。

总结

我的感受操做栈数据结构相对于其余的数据结构来讲,仍是比较简单的。无论用对象仍是数组均可以实现栈数据结构,只要听从LIFO(后进先出)原则便可。喜欢的掘友能够点击关注+点赞哦!后面会持续更新其余数据结构,也把本身学的知识分享给你们。固然写做也能够当成复盘。2021年加油!实现本身的目标。

相关文章
相关标签/搜索