前言
本人是一个刚入行的菜鸡前端程序员,写这个文章的目的只是为了记录自己学习的笔记与成果,如有不足请大家多多指点。
上一篇文章我们学习了如何创建和使用计算机科学中最常用的数据结构——数组。我们知道,可以在数组的任意位置上删除或添加元素。然而,有时候还需要一种能在添加或删除元素时进行更多控制的数据结构。有两种类似于数组的数据结构在添加和删除元素时更为可控,它们就时栈和队列。本篇文章中我们先讲栈。
栈数据结构
栈是一种遵循后进先出(LIFO(last-in,first-out))原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。

创建一个基于数组的栈
我们将创建一个类来表示栈
class Stack {
constructor() {
this.items = [] //我们需要一种数据结构来保存栈里的元素
}
}
接下来,为栈声明一些方法
- push(element(s)): 添加一个(或几个)新元素到栈顶
- pop() : 移除栈顶的元素,同时返回被移除的元素
- peek(): 返回栈顶的元素,不对栈做任何的修改(该方法不会移除栈顶的元素,仅仅返回它)
- isEmpty():如果栈里面没有任何元素就返回true,否则返回false
- clear(): 移除栈里的所有元素
- size(): 返回栈里的元素个数。该方法和数组的length属性很类似
向栈添加元素 - push()
向栈里添加新元素,有一点很重要:该方法智能添加元素到栈顶,也就是栈的末尾。push方法可以这样写
push(element) {
this.items.push(element)
}
因为我们使用了数组来保存栈里的元素,所以可以直接使用数组的push方法来实现
从栈移除元素 - pop()
实现pop方法。用来移除栈里的元素。栈遵循LIFO原则,移出的时最后添加进去的元素。
pop() {
return this.items.pop()
}
只能用push和pop方法来添加和删除栈中的元素,这样我们的栈就遵从了LIFO原则
查看栈的元素项 - peek()
peek方法。返回栈顶的元素
peek() {
return this.items[this.items.length - 1]
}
因为类内部是用数组保存元素的,所以访问数组的最后一个元素可以使用 length - 1
检查栈是否为空 - isEmpty()
isEmpty,如果栈为空的话返回 true , 否则就返回 false。
isEmpty() {
return this.items.length === 0
}
使用isEmpty方法,我们能简单地判断内部数组的长度是否为0。
返回栈的长度 - size()
类似于数组的 length 属性,我们也能实现栈的 length。
size() {
return this.items.length
}
清空栈元素 - clear()
clear 方法用来移除栈里所有的元素,把栈清空
clear() {
this.items = []
}
也可以多次调用pop方法,直到把数组中的元素全部移除
clear() {
if(this.items.size) {
this.items.pop()
}
}
创建一个基于 JavaScript 对象的 Stack 类
创建一个 Stack 类最简单的方式是使用一个数组来存储其元素。在处理大量数据的时候,我们同样需要评估如何操作数据是最高效的。如果我们能直接获取元素,占用较少的空间,并且仍然保证所有元素按照我们的需要排列,那不是更好吗?我们也可以使用一个JavaScript对象来存储所有的栈元素,保证它们的顺序并且遵循LIFO原则。
首先像下面这样声明一个 Stack 类
class Stack {
constructor() {
this.count = 0;
this.items = {};
}
}
向栈中插入元素 - push()
在基于数组的版本中,我们可以同时向 Stack 类中添加多个元素。由于现在使用了一个对象,这个版本的 push 方法只允许我们一次插入一个元素。
push(element) {
this.items[this.count] = element
this.count++;
}
在JavaScript中,对象是一系列键值对的集合。要向栈中添加元素,我们将使用count变量作为items对象的键名,插入的元素则是它的值。在向栈中插入元素后,我们递增 count 变量。
验证一个栈是否为空和它的大小 - isEmpty(), size()
count 属性也表示栈都的大小
size() {
return this.count
}
要验证栈是否为空,可以直接判断 count 的值是否为 0
isEmpty() {
return this.count === 0
}
从栈中弹出元素 - pop()
由于我们没有使用数组来存储元素,需要手动实现移除元素的逻辑。pop方法同样返回了栈中移除的元素。
pop() {
if(this.isEmpty()) { // 检验栈是否为空,如果为空返回undefined
return undefined;
}
this.count--; //如果栈不为空的话,count属性减1
const result = this.items[this.count]; //保存栈顶的值
delete this.items[this.count] //删除栈顶的值
return result; //返回保存的栈顶的值
}
由于我们使用的是 JavaScript 对象,可以用 delete 运算符从对象中删除特定的值
查看栈顶的值并将栈清空 - peek(), clear()
// 查看栈顶的元素
peek() {
if(this.isEmpty()) {
return undefined;
}
return this.items[this.count - 1];
}
// 清空栈
clear() {
this.items = {};
this.count = 0;
}
// 我们也可以遵循LIFO原则,使用下面的逻辑来移除栈中所有的元素
while(!this.isEmpty()) {
this.pop()
}
创建 toString 方法
在数组的版本中,我们不需要关心 toString 方法的实现,因为数组结构可以直接使用数组已经提供的 toString 方法。对于使用对象的版本,我们将创建一个 toString 方法来像数组一样打印出栈的内容。
toString() {
if(this.isEmpty()) {
return ''; // 如果栈是空的,我们只需要返回一个空的字符串
}
let objString = `${this.items[0]}`; // 如果不是空的,需要用它底部的第一个元素作为字符串的初始值
for(let i = 1; i < this.count; i++) {
objString = `${objString},${this.items[i]}`;
}
return objString;
}
保护数据结构内部元素
在创建别的开发者也可以使用的数据结构或对象时,我们希望保护内部的元素,只有我们暴露出的方法才能修改内部结构。对于Stack类来说,要确保元素只会被添加到栈顶,而不是栈底或其他任意位置。但是,我们在Stack类中声明的items 和 count 属性没有得到保护,因为JavaScript的类就是这样工作的。
const stack = new Stack();
console.log(Object.getOwnPropertyNames(stack)); // ['count', 'items'] (1)
console.log(Object.keys(stack)); // ['count', 'items'] (2)
console.log(stack.items); // {} (3)
通过 (1) ,(2) 我们可以知道 count 和 items 属性是公开的,我们可以像 (3) 那样直接访问它们。根据这种行为,我们可以对这两个属性赋新的值。
下划线命名约定
一部分开发者喜欢在JavaScript中使用下划线命名约定来标记一个属性为私有属性。
class Stack {
constructor() {
this._count = 0;
this._items = {}
}
}
下划线命名约定就是在属性名称之前加上一个下划线(_)。不过这种方式只是一种约定,并不能保护数据,而且只能依赖于使用我们代码的开发者所具备的常识。
用ES2015的限定作用域 Symbol 实现类
ES2015新增了一种叫做 Symbol 的基本类型,它是不可变的,可以用作对象的属性。
const _items = Symbol('stackItems');// 声明 Symbol 类型的变量 _items
// 我们使用数组的形式来创建Stack类以简化代码
class Stack {
constructor() {
this[_items] = [] // 要访问_items,只需要把所有的 this.items 都换成 this[_items]
}
// 栈的方法
}
用栈解决问题
从十进制到二进制
要把十进制转化为二进制,我们可以将该十进制除以2(二进制是满二进一)并对商取整,知道结果为0为止。过程大概是如下这样

function decimalToBinary(decNumber) {
const remStack = new Stack()
let number = decNumger;
let rem;
let binaryString = '';
while (number > 0) { // 当除的结果不为0时
rem = Math.floor(number % 2);
remStck.push(rem); // 将余数放入到栈中
number = Math.floor(number / 2) ; //仅返回除法运算结果的整数部分
}
while (!remStack.isEmpty()) {
binaryString += remStack.pop().toString() //用pop方法将栈中的元素都移除,把出栈的元素连接成字符串
}
return binaryString;
}
进制转换算法
我们还可以修改上面的算法,使之能把十进制转换为基数为 2 ~ 36 的任意进制
function baseConverter(decNumber, base) {
const remStack = new Stack();
//2 ~ 36 需要对栈中的数字进行转化,从十一进制开始,字母表中的每个字母将表示相应的基数
const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
let number = decNumber;
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.isEmpty()) {
baseString += digits[remStack.pop()];
}
return baseString;
}