一、概念(Stack)
栈一种数据呈线性排列的数据结构,不过在这种结构中,我们只能访问最新添加的数 据。栈就像是一摞书,拿到新书时我们会把它放在书堆的最上面,取书时也只能从最上面的新 书开始取。
二、特点
- LIFO(Last In First Out),即最后添加的数据最先被取出
- 线性结构排列
数据排列成一条线,这条线上的数据具有一定的前后关系,并且最多只有前后两个方向。常见的线性表有:数组、队列、栈、链表。 非线性表中数据并不是简单的前后关系,比如二叉树,图(例如求最短路径的)等。
三、Javascript简单实现
首先,列出一个栈应该具备的基本方法:
- push(element(s)):添加一个(或几个)新元素到栈顶。
- pop():移除栈顶的元素,同时返回被移除的元素。
- peek():返回栈顶的元素,不对栈做任何的修改(这个方法不会移除栈顶的元素,仅仅是返回它)
- isEmpty():如果栈里没有任何元素都返回true,否则返回false。
- clear():移除栈里的所有元素
- size():返回栈里的元素个数,这个方法和数组的length属性很类似。 直接上ES6代码:
class Stack{
constructor(){
this.items = [];
}
push(item){
this.items.push(item);
}
pop(){
return this.items.pop();
}
peek(){
return this.items.length ? this.items[this.items.length - 1] : undefined;
}
isEmpty(){
return items.length === 0;
}
clear(){
this.items = [];
}
size(){
return this.items.length;
}
}
四、思考和优化
如果你觉得上述代码就能实现一个Stack类?naive! 来看如下代码:
let stack = new Stack();
stack.push(1);
stack.push(2);
stack.items;
结果输出
Stack: (2) [1, 2]
也就是说,我们应该每次只能通过peek、push、pop等方法来操作获取栈的信息。
这么直接stack.items访问很明显是不合理的。所以应该把items属性设置为私有的才行。
思路如下:
- 1、将items 从constructor中移除
- 2、用闭包,保证items在被引用的同时不被回收,即用私有变量取代类中属性
直接上代码:
let Stack = (function () {
const items = new WeakMap();
class Stack {
constructor () {
items.set(this, []);
}
push(item){
let s = items.get(this);
s.push(item);
}
pop(){
let s = items.get(this);
return s.pop();
}
peek(){
let s = items.get(this);
return s.length ? s[s.length - 1] : undefined;
}
isEmpty(){
let s = items.get(this);
return s.length === 0;
}
clear(){
let s = items.get(this);
s = [];
}
size(){
let s = items.get(this);
return s.length;
}
}
return Stack;
})();
这样,就可以避免通过实例来访问栈的私有属性了。
顺便再提一下,为什么要用到WeakMap:
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
1、WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的。
2、正由于这样的弱引用,WeakMap 的 key 是不可枚举的 (没有方法能给出所有的 key)。如果key 是可枚举的话,其列表将会受垃圾回收机制的影响,从而得到不确定的结果。因此,如果你想要这种类型对象的 key 值的列表,你应该使用 Map。
也即是说,我们把一个数组,放到WeakMap中,不可枚举,遍历不到,这就达到了私有变量的效果了。
五、栈的实际应用
1、进制转换
10进制8转化为2进制数
function conver(num, radix) {
let stack = new Stack()
let binaryString = ''
let digits = '0123456789ABCDEF'
while (num > 0) {
stack.push(num % radix)
num = parseInt(num / radix)
}
while (!stack.isEmpty()) {
binaryString += digits[stack.pop()]
}
console.log(binaryString)
}
conver(8, 2) // 输出:1000
2~9之间的通用的进制转换
const convertHex = (num, base) => {
var stack = new Stack();
while (num > 0) {
stack.push(num % base);
num = Math.floor(num / base);
}
let converted = "";
while (stack.length() > 0) {
converted += stack.pop();
}
return converted;
}
2、判断是否是回文
回文是指这样一种现象:一个单词、短语或数字,从前往后写和从后往前写都是一样的。比如"ABCBA"、"12321"
const isPalindrome = (str) => {
var stack = new Stack();
str += ''; // 转换为字符串
for (let i = 0, i< str.length; i++) {
stack.push(str[i]);
}
let reverseWord = "";
while (stack.size() > 0) {
reverseWord += stack.pop();
}
return str === reverseWord;
}
3、有效的括号
"()[]{}" 是有效闭合的。 "([]{})" 是有效闭合的。但是 "([)]{}" 不是有效闭合的。
思路如下:
- 1、碰到左括号,压入栈中
- 2、碰到右括号,直接将原来栈顶的元素给pop出来。
- 3、如果最终栈的元素为空,说明压入到栈的左括号都有遇到正确的闭合的右括号,被pop出来了。反之,则说明栈里面没有正确的闭合,或者一开始就先压入右括号。
const isValid = str => {
const stack = new Stack();
for(let i = 0;i<str.length;i++){
if(str[i] === '(' || str[i] === '[' || str[i] === '{'){
stack.push(str[i]);
}else if((str[i] === ')' || str[i] === ']' || str[i] === '}')
){
stack.size() && stack.pop();
}
}
return stack.size() === 0;
}
六、结语
要想在互联网的浩浩人海中卷出优势,必须得学好算法。 要想学好算法,得先了解基本的数据结构。 一点点的进步,今天先学好Stack栈。