携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第20天,点击查看活动详情
栈
栈数据结构
栈是一种遵从先进后出原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。
创建一个基于数组的栈
class Stack{
constructor(){
this.items = [];
}
}
数组来保存栈里的元素。数组允许我们在任何位置添加或删除元素,由于栈遵循后进先出的原则,需要对元素的插入和删除功能进行限制。可以为栈声明一些方法:
push(element): 添加一个或多个元素到栈顶
pop(): 移出栈顶的元素,同时返回被移除的元素
peek(): 返回栈顶的元素,不对栈做任何修改
isEmpty(): 如果栈里没有任何元素就返回true,否则返回false
clear(): 移除栈里的所有元素
size(): 返回栈里的元素个数。
向栈添加元素
push(element){
this.items.push(element);
}
从栈移除元素
pop(){
return this.items.pop();
}
查看栈顶元素
peek(){
return this.items[items.length - 1]
}
判断栈是否为空
isEmpty(){
return this.items.length === 0;
}
清空栈
clear(){
this.items = [];
}
查看栈元素个数
size(){
return this.items.length;
}
使用Stack类
const stack = new Stack();
//判断栈是否为空
console.log(stack.isEmpty()); //true
//给栈里添加一些元素
stack.push(5);
stack.push(8);
//查看栈顶元素
console.log(stack.peek()); //8
stack.push(11);
//查看栈中元素个数
console.log(stack.size()); // 3
//从栈中移出两个元素
stack.pop();
stack.pop();
//查看栈中元素个数
console.log(stack.size()); // 1
创建一个基于JavaScript对象的Stack类
创建一个Stack类最简单的方式使用一个数组来存储元素。但是在处理大量数据时,这并不是最高效的。在使用数组时,大部分方法的时间复杂度是 O(n)。O(n) 的意思是,迭代整个数组直到找到要找的那个元素,在最坏的情况下需要迭代数组的所有位置, n 代表数组的长度。另外,数组是一个有序集合,为了保证元素的排列有序,会占用更多的内存空间。
如果能直接获取元素,占用较少的内存空间,并且也能保证元素按照我们想要的顺序排列,那不是更好吗?可以使用JavaScript对象来存储所有的栈元素,保证它们的顺序并且遵循LIFO原则。
声明一个Stack类( stack.js 文件)
class Stack {
constructor() {
this.count = 0;
this.items = {};
}
}
向栈中插入元素
在数组版本中,可以同时向Stack类中添加多个元素。由于现在使用了对象,我们只能一次添加一个元素。
push(el){
this.items.[this.count] = el;
this.count++;
}
对象是一种键值对的集合。在向栈中添加元素时,使用count变量作为 items 对象的键名,插入的元素则是它的值。插入元素后,递增count变量。
创建一个实例:
const stack = new Stack();
stack.push("5");
stack.push("8");
console.log(stack);
验证一个栈是否为空和它的大小
count 属性也表示栈的大小,所以可以返回 count 的值来实现 size 方法,验证栈是否为空,可以判断 count 的值是否为0。
size(){
return this.count;
}
isEmpty(){
return this.count === 0;
}
console.log(stack.size());
console.log(stack.isEmpty());
从栈中弹出元素
由于没有使用数组存储元素,需要手动实现移出元素的逻辑。
首先,检验栈是否为空,如果为空就直接返回 undefined。如果不为空,将 count 属性减一,取出保存栈顶的值,最后在对象中删除这个元素。
pop(){
if(this.isEmpty()){
return undefined;
}
this.count--;
const res = this.items[this.count];
delete this.items[this.count];
return res;
}
现在栈中的数据是
Stack { count: 2, items: { '0': 5, '1': 8 } },要删除栈顶的元素,需要把 count 属性从2减为1,这样就能找到8,删除它并返回它console.log(stack.pop());。
查看栈顶的值和清空栈
peek(){
if(this.isEmpty()){
return undefined;
}
return this.items[this.count-1];
}
clear(){
this.count = 0;
this.items = {};
}
也可以遵循LIFO原则,使用下面的逻辑来移除栈中的所有元素。
while(!this.isEmpty()){
this.pop();
}
console.log(stack.peek());
console.log(stack.clear());
console.log(stack);
创建toString方法
数组版本中,数组已经已经提供了 toString 方法,可以直接使用它。对象版本需要创建一个 toString 方法来像数组一样打印出栈的内容。
toString() {
if (this.isEmpty()) {
return "";
}
let str = `${this.items[0]}`;
for (let i = 1; i < this.count; i++) {
str += `,${this.items[i]}`;
}
return str;
}
如果栈为空,我们只需要返回一个空字符串。如果不是空的,把第一个元素变成字符串赋值给str,然后遍历到栈顶。
除了toString方法,创建的其他方法的复杂度均为 O(1) ,可以直接找到目标元素对其操作。
保护数据结构内部元素
现在创建好了 Stack 类,但是类中声明的 items 和count 并没有得到保护,可以不通过类暴露的方法对属性直接进行修改。
cosnt stack = new Stack;
stack.push(5);
stack.items = {};
stack.count = 0;
console.log(stack);
基于原型的类能节省内存空间并在扩展方面优于基于函数的类,但这种方式并不能声明私有属性或方法。
用 Symbol 实现类
ES6 新增了 Symbol 基本数据类型,它是唯一的,可以用做对象的属性。
const _items = Symbol("stackitems");
class Stack {
constructor() {
this[_items] = [];
}
//栈的方法
push(element) {
this[_items].push(element);
}
pop() {
return this[_items].pop();
}
peek() {
return this[_items][this[_items].length - 1];
}
isEmpty() {
return this[_items].length === 0;
}
clear() {
this[_items] = [];
}
size() {
return this[_items].length;
}
}
const stack = new Stack();
stack.push(5);
stack.push(8);
console.log(stack);
let obj_symbol = Object.getOwnPropertySymbols(stack);
console.log(obj_symbol);
console.log(obj_symbol[0]);
stack[obj_symbol[0]].push(1);
console.log(stack);
从代码中可以看出,执行
stack[obj_symbol[0]]是可以得到 _items 的,并且因为它是一个数组,可以进行任意的数组操作。
用WeakMap实现类
WeakMap 可以确保属性是私有的,它可以存储键值对,其中键是对象,值可以是任意数据类型。
const items = new WeakMap
class Stack{
constructor(){
items.set(this,[]);
}
push(el){
const s = items.get(this);
s.push(el);
}
pop(){
const s = items.get(this);
const r = s.pop();
return r;
}
//其他方法
}
const stack = new Stack();
stack.push(5);
stack.push(8);
console.log(stack);
现在 items 在stack 类中是真正的私有属性,但是这种方法的可读性不强,而且在扩展该类时无法继承私有属性。
用栈解决实际问题
十进制转二进制
要把十进制转换成二进制,需要将十进制的数一直除以2,然后对商取整,直到结果为0,举个栗子,将10转换成2进制:
算法如下:
function decimalToBinary(decNumber) {
const remStack = new Stack();
let number = decNumber;
let rem;
let binaryString = "";
while (number > 0) {
rem = Math.floor(number % 2);
remStack.push(rem);
number = Math.floor(number / 2);
}
while (!remStack.isEmpty()) {
binaryString += remStack.pop().toString();
}
return binaryString;
}