这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战
栈
栈是一种后进先出(LIFO)原则的有序集合,是一种线性结构。新添加和待删除的元素都保存在栈的一端,叫做栈顶,另一端叫做栈底。最常见的的例子就是叠放的书籍等,也被用于浏览器的历史记录功能。
功能
栈是一种基本的数据结构,在日常工作中经常使用,我们可以来思考一下如何来创建一个栈呢,一个栈到底有哪些功能呢?实现功能如下:
- push(ele(s)): 添加一个或多个新元素到栈顶
- pop(): 移除栈顶元素,并返回被移除元素
- peek(): 返回栈顶元素,不对栈做任何修改
- isEmpty(): 栈是否为空
- clear(): 移除栈里所有元素
- size(): 返回栈中元素个数
实现
看到上面push和pop两个方法,大家是不是就觉得很像我们的数组呢?那我们来如何实现它呢,下面介绍了两种方法,实现起来非常简单。
基于数组的方式
数组创建一个Stack类是最简单的,相关代码也比较简单,可以直接阅读代码
class Stack {
constructor() {
this._value = [];
}
push(...eles) {
this._value.push(...eles)
}
pop() {
return this._value.pop()
}
peek() {
return this._value[this.size - 1]
}
isEmpty() {
return this.size() === 0;
}
clear() {
this._value = [];
}
size() {
return this._value.length;
}
}
const stack = new Stack()
stack.push(1,2,3)
console.log(stack); // [1, 2, 3]
基于JavaScript对象的方式
上面第一种方式是以数组来创建一个Stack类的,他是最简单的方式来存储其元素,而我们为什么又要使用对象来实现Stack类呢?当数据量过大时,我们要考虑如何操作数据是最高效的。在使用数组时,大部分的方法都是O(n),数据量大的时候所需时间较长。我们如果能够直接获取元素,占用空间较少并且能到保证所有元素按照我们的需要排列不更好嘛?是的,所以我们使用对象来实现这一功能,可是我们知道对象的属性是无序的,那怎么让他们有序呢?下面使用了this._count = 0 来约束他, this._count依次递增,保证元素有素,从而遵循LIFO的原则。
class ObjectStack {
constructor() {
this._value = {};
this._count = 0;
}
push(...eles) {
eles.forEach(ele => {
this._value[this._count] = ele;
this._count++;
})
}
pop() {
if (this.isEmpty()) return undefined;
this._count--;
const result = this._value[this._count];
delete this._value[this._count];
return result;
}
peek() {
if (this.isEmpty()) return undefined;
return this._value[this._count - 1]
}
isEmpty() {
return this._count === 0;
}
clear() {
this._value = {};
this._count = 0;
}
size() {
return this.__count;
}
toString() {
if (this.isEmpty()) return '';
let str = `${this._value[0]}`;
for(let i = 1; i < this._count; i++) {
str = `${str},${this._value[i]}`;
}
return str;
}
}
const objectStack = new ObjectStack()
objectStack.push(1,2,3)
console.log(objectStack); // { '0': 1, '1': 2, '2': 3 }
除了以上方法,还有其他什么方法实现呢?我们是不是可以考虑Symbol、WeakMap呢,为什么会提到这,因为如今这两种方式中this._value和this._count是不受保护的,用户可以直接获取到然后进行修改赋值,不能确保新添加的元素只会添加到栈顶,还可能被添加到其他地方,这是不安全的,所以我们可以修改实现方式让他更加完善,大家可以尝试一下。
应用
栈的应用十分广泛,在这我们以进制转换为例进行讲解。最常见的就是十进制转二进制,那这是如何实现的呢?先来分析一下思路:
- 将待转换的数字除以进制数,结果不为0时,得到余数,将余数传入栈中
- 将待转换的数字除以进制数取整后循环第一步,直至结果为0
- 将栈中数字一次取出就可以组成转换后的数字
具体实现如下:
/**
*
* @param {*} descNumber 要转换的数字
* @param {*} base 进制
* @returns 转换后的数字
*/
const baseConverter = (descNumber, base) => {
const stack = new Stack();
const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
let number = descNumber;
let baseStr = '';
if (!(base >= 2 && base <= 36)) return '';
while(number > 0) {
const rem = number % base;
stack.push(rem);
number = Math.floor(number / base);
}
while(!stack.isEmpty()) {
baseStr = baseStr + digits[stack._value.pop()]
}
return baseStr
}
console.log(baseConverter(10, 2)); // 1010
console.log(baseConverter(10, 16)); // A