1.栈数据结构
栈是一种遵从后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。
在现实生活中也能发现许多栈的例子。例如,下图中的一摞书。
栈也被用在编程语言的编译器和内存中保存变量、方法调用等,浏览器历史记录。
1.1 创建一个基于数组的栈
我们需要一种数据结构来保存栈里的元素。可以选择数组。数组可以在任何位置添加或删除元素。由于栈遵循LIFO原则,需要对元素的插入和删除功能进行限制。也要为栈声明一些方法。
创建一个类来表示栈。
class Stack {
constructor() {
this.items = []
}
// 添加新元素到栈顶
push(element) {
return 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
}
// 清空栈里的所有元素
clear() {
return this.items = []
}
// 返回栈里的元素个数。该方法和数组的length属性很类似
size() {
return this.items.length
}
}
1.2 使用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
console.log(stack.isEmpty) // 输出false
我们往栈里添加了11,如果调用了size方法,输出3,因为栈里有三个元素(5, 8,11),如果我们调用isEmpty方法,会看到false。最后,我们再添加一个元素。
stack.push(15)
下图描述了栈的当前状态
结果,连续两次调用pop方法,从栈里移除两个元素
stack.pop()
stack.pop()
console.log(stack.size()) // 输出 2
在两次调用pop方法前,我们的栈里有四个元素。调用两次后,现在栈里仅剩下5和8了。如下图所示:
1.3 使用JavaScript对象创建Stack类
使用数组时,大部分的时间复杂度是O(n)。O(n)的意思是,我们需要迭代整个数组直到找到的那个元素。另外数组时元素的一个有序集合,为了保证元素的排列有序,会占用更多空间。
创建Stack类如下:
class Stack {
constructor() {
this.count = 0
this.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
}
// 清空栈里的所有元素
clear () {
this.items = {}
this.count = 0
}
// 返回栈里的元素个数。该方法和数组的length属性很类似
size () {
return this.count
}
// 方法类似于数组的toString()
toString() {
if (this.isEmpty()) {
return ''
}
let str = ''
for (let i = 0; i < this.count; i++) {
str += this.items[i]
}
return str
}
}
2. 保护数据结构内部元素
对于Stack类来说,要确保元素只会被添加到栈顶。而不是栈底或其他位置。
2.1 下划线命名
下划线命名约定就是在属性名称之前加上一个(_)。不过这种方式只是一种约定,并不能保护数据,而且只能依赖于使用我们开发者具备的常识。
class Stack {
constructor() {
this._count = 0;
this._items = {};
}
}
2.2 用symbol实现类
es2015新增了一种叫做Symbol的基本类型,它是不可变的,可以用作对象的属性。
const _items = Symbol('stackItems'); // {1}
class Stack {
constructor () {
this[_items] = []; // {2}
}
// 栈的方法
}
这种方法创建了一个假的私有属性,因为 ES2015 新增的 Object.getOwnPropertySymbols 方法能够取到类里面声明的所有 Symbols 属性。
const stack = new Stack();
stack.push(5);
stack.push(8);
let objectSymbols = Object.getOwnPropertySymbols(stack);
console.log(objectSymbols.length); // 输出 1
console.log(objectSymbols); // [Symbol()]
console.log(objectSymbols[0]); // Symbol()
stack[objectSymbols[0]].push(1);
stack.print(); // 输出 5, 8, 1
从以上代码可以看到,访问 stack[objectSymbols[0]]是可以得到_items 的。并且,
_items 属性是一个数组,可以进行任意的数组操作,比如从中间删除或添加元素(使用对象进
行存储也是一样的)。但我们操作的是栈,不应该出现这种行为
2.3 用weakMap实现类
有一种数据类型可以确保属性是私有的,这就是 WeakMap。
const items = new WeakMap(); // {1}
class Stack {
constructor () {
items.set(this, []); // {2}
}
push(element){
const s = items.get(this); // {3}
s.push(element);
}
pop(){
const s = items.get(this);
const r = s.pop();
return r;
}
// 其他方法
}
items 在 Stack 类里是真正的私有属性。采用这种方法,代码的可读性
不强,而且在扩展该类时无法继承私有属性。
3. 用栈解决问题
编写算法,使之能把十进制转换成基数为 2~36 的任意进制。
function baseConverter(decNumber, base) {
const remStack = new Stack();
const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // {6}
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()]; // {7}
}
return baseString;
}
使用算法,输出结果如下
console.log(baseConverter(100345, 2)); // 11000011111111001
console.log(baseConverter(100345, 8)); // 303771
console.log(baseConverter(100345, 10)); // 100345
console.log(baseConverter(100345, 16)); // 187F9
console.log(baseConverter(100345, 35)); // 2BW0