栈数据结构
栈是一种遵从后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。
接下来,我们创建一个类来表示栈。新建一个 stack-array.js 文件
const Stack {
constructor() {
this.items = [];
}
// 下面是为栈声明一些方法
push(element) {
// 添加新元素到栈顶
this.items.push(element);
}
pop() {
// 移除栈顶的元素,同时返回被移除的元素
return this.items.pop();
}
peek() {
// 返回栈顶的元素,不对栈做任何修改(该方法不会移除栈顶的元素,仅仅返回它)
return this.items[this.items.length - 1];
}
isEmpty() {
// 如果栈里没有任何元素就返回true,否则返回false。
return this.items.length === 0;
}
clear() {
// 移除栈里的所有元素
this.items = [];
}
size() {
// 返回栈里的元素个数。该方法和数组的length属性很类似
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
console.log(stack.isEmpty()); // 输出false
stack.push(15);
stack.pop();
stack.pop();
console.log(stack.size()); // 输出2
日常开发中,我们如果要创建别的开发者也可以使用的数据结构或对象时,我们希望保护内部的元素,只有我们暴露出的方法才能修改内部结构。对于Strack类来说,要确保元素只会被添加到栈顶,而不是栈底或其他任意位置(比如栈的中间)。但是,我们刚才在Stack类中声明的items属性并没有得到保护,因为JavaScript的类就是这样工作的。
看下面的代码
const stack = new Stack();
console.log(Object.getOwnPropertyNames(stack)); // 输出["items"]
console.log(Object.keys(stack)); 输出["items"]
console.log(stack.items); // 输出[]
我们可以直接访问到items属性,也可以直接对这个属性赋新的值。我们希望Stack类中用户只能访问我们在类中暴露的方法。下面来看看使用JavaScript来实现私有属性的方法。
使用weakMap数据结构,修改我们的stack-array.js文件
WeakMap可以存储键值对,其中键是对象,值可以是任意数据类型
const items = new WeakMap(); // 声明一个WeakMap类型的变量items
const Stack {
constructor() {
items.set(this, []); // 在constructor中,以this(Stack类自己的引用)为键,把代表栈的数组存入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;
}
peek() {
const s = items.get(this);
return s[s.length - 1];
}
isEmpty() {
const s = items.get(this);
return s.length === 0;
}
size() {
const s = items.get(this);
return s.length;
}
clear() {
items.set(this, []);
}
print() {
cosnt s = items.get(this);
console.log(s.toString());
}
}
再来执行下面的代码
const stack = new Stack();
console.log(Object.getOwnPropertyNames(stack)); // 输出[]
console.log(Object.keys(stack)); 输出[]
console.log(stack.items); // 输出undefined
stack.push(5);
stack.push(8);
stack.print(); // 输出5,8
由此,items在Stack类里是真正的私有属性。但是采用这种方法,代码的可读性不强,而且在扩展该类时无法继承私有属性。
事实上,我们不能像在其他编程语言中一样声明私有属性和方法。虽然有很多种方法都可以达到相同的效果,但无论是在语法还是性能层面,这些方法都有各自的有点和缺点。
下面举一个应用栈的小实例(进制转换)
从十进制到二进制
日常开发中,我们主要使用十进制。但在计算科学中,二进制非常重要,因为计算机里的所有内容都是用二进制数字表示的(0和1)。要把十进制转化成二进制,我们可以将该十进制数除以2(二进制是满二进一)并对商取整,直到结果是0为止。例如,把十进制的数10转化成二进制的数字,过程大概如下:
下面写一下实现十进制转二进制的方法
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); // 让number除以2,并向下取整以便循环运算
}
while(!remStack.isEmpty()) {
binaryString += remStack.pop().toString(); // 用pop方法把栈中的元素移除,并把出栈的元素连接成字符串
}
return binaryString;
}
console.log(decimalToBinary(233)); // 输出11101001
接下来我们把上面的方法改造一下,实现十进制转换成基数为 2~36的任意进制。
function baseConverter(decNumber, base) {
const remStack = new Stack();
// 从十一进制开始,字母表中的每个字母将表示相应的基数。字母A代表基数11,字母B代表基数12,以此类推
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(baseConverter(100345, 2)); // 输出 11000011111111001
console.log(baseConverter(100345, 8)); // 输出 303771
console.log(baseConverter(100345, 16)); // 输出 187F9
console.log(baseConverter(100345, 35)); // 输出 2BWO