栈是一种遵循从后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同一端称作栈顶,另一端就叫栈底。
在栈里,新的元素都靠近栈顶,旧的元素都接近栈底。
生活中栈的例子:一摞书,叠放的盘子。
栈也被用在编程语言的编译器和内存中保存变量、方法调用等。也被用于浏览器历史记录(浏览器的返回按钮)。
创建一个基于数组的栈
- 向栈添加元素
- 从栈移除元素
- 查看栈顶元素
- 检查栈是否为空
- 清空栈元素
- 使用
Stack类
/*** 创建一个基于数组的栈*/class Stack {constructor() {this.items = []; // 选择一种数据结构(数组)来保存栈里的元素}// 添加一个(或几个)新元素到栈顶push(element) {this.items.push(element);}// 移除栈顶的元素,同时返回被移除的元素pop() {return this.items.pop();}// 返回栈顶的元素,但不对栈做任何修改peek() {return this.items[this.items.length - 1];}// 如果栈里没有元素就返回 true,否则返回 falseisEmpty() {return this.items.length === 0;}// 返回栈里的元素个数size() {return this.items.length;}// 移除栈里的所有元素clear() {this.items = [];}}
/*** 使用 Stack 类*/// 初始化 Stack 类const stack = new Stack();// 验证栈是否为空console.log(stack.isEmpty()); // true// 往栈里添加元素stack.push(2);stack.push(5);stack.push(8);// 查看栈顶的元素console.log(stack.peek()); // 8// 查看栈里的元素个数console.log(stack.size()); // 3// 验证栈是否为空console.log(stack.isEmpty()); // false// 移除栈里的元素console.log(stack.pop()); // 8console.log(stack.pop()); // 5// 移除栈里的所有元素stack.clear();// 验证栈是否为空console.log(stack.isEmpty()); // trueconsole.log(stack.peek()); // undefinedconsole.log(stack.size()); // 0
创建一个基于 JavaScript 对象的 Srack 类
比基于数组的栈迭代占用的时间更少。
/*** 创建一个基于数组的栈*/class Stack {constructor() {this.count = 0;this.items = {};}// 向栈中插入元素// 在 JavaScript 中,对象是一系列键值对的集合。使用 count 变量作为 items 对象的键名,插入的元素则是它的值push(element) {this.items[this.count] = element;this.count++;}// 移除栈顶的元素,同时返回被移除的元素pop() {if (this.isEmpty()) {return undefined;}this.count--;const result = this.items[this.count];delete this.items[this.count];return result;}// 返回栈顶的元素,但不对栈做任何修改peek() {if (this.isEmpty()) {return undefined;}return this.items[this.count - 1];}// 如果栈里没有元素就返回 true,否则返回 falseisEmpty() {return this.count === 0;}// 返回栈里的元素个数size() {return this.count;}// 移除栈里的所有元素clear() {this.items = {};this.count = 0// while (!this.isEmpty()) {// this.pop();// }}// 创建一个 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 类const stack = new Stack();// 验证栈是否为空console.log(stack.isEmpty()); // true// 往栈里添加元素stack.push(2);stack.push(5);stack.push(8);// 打印栈的内容console.log(stack.toString()); // 2, 5, 8// 查看栈顶的元素console.log(stack.peek()); // 8// 查看栈里的元素个数console.log(stack.size()); // 3// 验证栈是否为空console.log(stack.isEmpty()); // false// 移除栈里的元素console.log(stack.pop()); // 8console.log(stack.pop()); // 5// 移除栈里的所有元素stack.clear();// 验证栈是否为空console.log(stack.isEmpty()); // trueconsole.log(stack.peek()); // undefinedconsole.log(stack.size()); // 0// 从下面行 {1} 和 行 {2} 得出:count 和 items 属性是公开的,我们可以像行 {3} 那样直接访问它们// 根据这种行为,可以对这两个属性赋新的值console.log(Object.getOwnPropertyNames(stack)); // [ 'count', 'items' ]console.log(Object.keys(stack)); // [ 'count', 'items' ]console.log(stack.items); // {}
在 Stack 类声明的 items 和 count 属性并没有得到保护。ES6 类是基于原型的,基于原型的类能节省内存空间并在扩展方面优于函数的类,但这种方式不能声明私有属性(变量)或方法。我们希望 Stack 类的用户只能访问我们在类中暴露的方法。
保护数据结构内部元素
1. 下划线命名约定
class Stack1 {constructor() {this._count = 0;this._items = {};}// 栈的方法}
这种方式只是一种约定,并不能保护数据,而且只能依赖于使用该类的开发者所具备的常识。
2. 用 ES6 的限定作用域 Symbol 实现 Stack 类
const _items = Symbol('stackItems'); // 声明 Symbol 类型的变量 _itemsclass Stack2 {constructor() {this[_items] = []; // 在类的构造函数中初始化它的值}// 栈的方法// 向栈中插入元素// 在 JavaScript 中,对象是一系列键值对的集合。使用 count 变量作为 items 对象的键名,插入的元素则是它的值push(element) {this[_items][this.count] = element;this.count++;}// 移除栈顶的元素,同时返回被移除的元素pop() {if (this.isEmpty()) {return undefined;}this.count--;const result = this[_items][this.count];delete this[_items][this.count];return result;}// 返回栈顶的元素,但不对栈做任何修改peek() {if (this.isEmpty()) {return undefined;}return this[_items][this.count - 1];}// 如果栈里没有元素就返回 true,否则返回 falseisEmpty() {return this.count === 0;}// 返回栈里的元素个数size() {return this.count;}// 移除栈里的所有元素clear() {this[_items] = {};this.count = 0// while (!this.isEmpty()) {// this.pop();// }}// 创建一个 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;}}const stack = new Stack2();stack.push(5);stack.push(8);let objectSymbols = Object.getOwnPropertySymbols(stack);console.log(objectSymbols.length); // 1console.log(objectSymbols); // [ Symbol(stackItems) ]console.log(objectSymbols[0]); // Symbol(stackItems)stack[objectSymbols[0]].push(2);console.log(stack.toString()); // 2
这种方式创建了一个假的私有属性,因为 ES6 新增的 Object.getOwnPropertySymbols 方法能够取到类里面声明的所有 Symbol 属性。
3. 用 ES6 的 WeakMap 实现 Stack 类
const items = new WeakMap(); // 声明一个 WeakMap 类型的变量 itemsclass Stack3 {constructor() {items.set(this, []); // 以 this(Stack3 类自己的引用)为键,把代表栈的数组存入 items}push(element) {const s = items.get(this); // 从 WeakMap 中取出值,以 this 为键从 items 中取值s.push(element);}pop() {const s = items.get(this);const r = s.pop();return r;}// 栈的其他方法}
这种方式,items 在 Stack 类里是真正的私有属性。但代码可读性不强,而且在扩展该类时无法继承私有属性。鱼与熊掌不可兼得!
4. 通过在属性前添加 # 作为前缀来声明私有属性
class Stack4 {#count = 0;#items = 0;// 栈的方法}
用栈解决进制转换问题
/*** 创建一个基于数组的栈*/class Stack {constructor() {this.items = []; // 选择一种数据结构(数组)来保存栈里的元素}// 添加一个(或几个)新元素到栈顶push(element) {this.items.push(element);}// 移除栈顶的元素,同时返回被移除的元素pop() {return this.items.pop();}// 返回栈顶的元素,但不对栈做任何修改peek() {return this.items[this.items.length - 1];}// 如果栈里没有元素就返回 true,否则返回 falseisEmpty() {return this.items.length === 0;}// 返回栈里的元素个数size() {return this.items.length;}// 移除栈里的所有元素clear() {this.items = [];}}
- 十进制转二进制
// 十进制转二进制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;}console.log(decimalToBinary(233)); // 11101001console.log(decimalToBinary(10)); // 1010console.log(decimalToBinary(1000)); // 1111101000
- 进制转换算法
// 进制转换算法function baseCnverter(decNumber, base) {const remStack = new Stack();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;}console.log(baseCnverter(100345, 2)); // 11000011111111001console.log(baseCnverter(100345, 8)); // 303771console.log(baseCnverter(100345, 16)); // 187F9console.log(baseCnverter(100345, 35)); // 2BW0
