栈是一种遵循从后进先出(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,否则返回 false
isEmpty() {
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()); // 8
console.log(stack.pop()); // 5
// 移除栈里的所有元素
stack.clear();
// 验证栈是否为空
console.log(stack.isEmpty()); // true
console.log(stack.peek()); // undefined
console.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,否则返回 false
isEmpty() {
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()); // 8
console.log(stack.pop()); // 5
// 移除栈里的所有元素
stack.clear();
// 验证栈是否为空
console.log(stack.isEmpty()); // true
console.log(stack.peek()); // undefined
console.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 类型的变量 _items
class 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,否则返回 false
isEmpty() {
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); // 1
console.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 类型的变量 items
class 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,否则返回 false
isEmpty() {
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)); // 11101001
console.log(decimalToBinary(10)); // 1010
console.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)); // 11000011111111001
console.log(baseCnverter(100345, 8)); // 303771
console.log(baseCnverter(100345, 16)); // 187F9
console.log(baseCnverter(100345, 35)); // 2BW0