前端数据结构(1)之栈及其应用

328 阅读3分钟

基础

  栈是一种特殊的线性表,它可以用数组或者链表来实现。只能在栈顶操作数据,具有先进后出的特点。
  假如有一个序列1,2,3,4,5。将它按顺序加入栈中,它的结构如下图:


  现实生活中,我们也能看到很多关于栈的结构,比如说网球桶,你拿到的网球都是桶顶的,放回去的时候也会放到桶顶。
  在Js中,像执行上下文也使用栈这种数据结构来维护的,栈底是全局作用域,当前执行代码的执行上下文依次加入栈中,栈顶的元素永远是正在执行的上下文对象。基本数据类型的值也放在栈中。
  在canvas标签中,有两个方法save()和restore()也用到了栈这种数据结构,save方法将canvas当前绘图环境的所有属性压栈,之后可以改变一些属性做某些操作,当你想恢复之前保存的绘图环境时,调用restore方法将canvas状态堆栈的顶部条目弹出,就实现了canvas绘图环境的保存和恢复。

栈的实现

  我用数组实现了栈这种数据结构,因为js中数组方法实现了push和pop的方法,再来实现栈会非常的简单。
  操作栈的方法有:push(将元素加入栈顶),pop(将元素从栈顶删除),top(返回栈顶元素),size(返回栈里元素的个数),isEmpty(判断栈是否为空),clear(清空栈中的元素)。

const Stack = function() {
    let items = [];
    this.push = function(item) {
        items.push(item);
    }
    this.pop = function() {
        return items.pop();
    }
    this.top = function() {
        return items[items.length - 1];
    }
    this.size = function() {
        return items.length;
    }
    this.isEmpty = function() {
        return items.length == 0;
    }
    this.clear = function() {
        items = [];
    }
}

栈的应用题

  1. 括号匹配

  合法的括号字符串应该是一个左括号就能匹配到一个对应的右括号。而且字符串中还会出现除了括号以外的的字符。如果用数组来解决这个问题的话,那么我们遍历到一个左括号,之后哪个右括号是它的右括号我们很难寻找。如果用栈来做思路就会非常简单。
  遇到左括号,将其压栈;遇到右括号,去栈顶元素,若栈为空,说明它没有对应的左括号,返回false,若栈顶元素是与它匹配的左括号,则继续遍历字符串;若遍历完字符串,栈不为空,则左括号多余了,返回false。

const isMatch = function(string) {
    let stack = new Stack();
    let obj = {
        ")": "(",
        "}": "{",
        "]": "["
    }
    for (value of string) {
        if (value == "(" || value == "[" || value == "{") {
            stack.push(value);
        } else if (value == ")" || value == "]" || value == "}") {
            if (stack.size()) {
                let top = stack.pop();
                if (top !== obj[value]) {
                    return false;
                }
            } else {
                return false;
            }
        }
    }
    if (stack.size() !== 0) {
        return false;
    }
    return true;
}

2.实现一个有求最小值方法的栈

  每次栈中的数据改变,那么这个栈的最小值很可能会改变。我们可以考虑用栈来存储最小值,这个最小值栈会在每次pop和pop时更新,它的栈顶的数据永远是此时数据栈中的最小值。

const minStack = function() {
    let items = [];
    let min_stack = new Stack();
    this.push = function(item) {
        if (items.length == 0) {
            items.push(item);
            min_stack.push(item);
        } else {
            if (item < min_stack.top()) {
                min_stack.push(item);
            } else {
                min_stack.push(min_stack.top());
            }
            items.push(item);
        }
    }

    this.pop = function() {
        min_stack.pop();
        return items.pop();
    }

    this.min = function() {
        return min_stack.pop();
    }
}

3.使用非递归的方法实现树的前序遍历
  前序遍历的顺序是先遍历当前节点,然后遍历它的左子树,最后再遍历它的的右子树。当遍历完左子树怎么找到右子树是这个问题的关键,我们考虑用栈存储右子树,当遍历完左子树,则弹出栈顶的一个右子树节点继续遍历。同理,迭代法如何实现树的中序遍历和后序遍历是一样的思路,可能实现的时候会比前序遍历难一些。

let pre_order2 = function(node) {
    if(node == null){
        return;
    }
    let stack = new Stack();
    let curr_node = node;
    while (curr_node) {
        console.log(curr_node.data);
        if (curr_node.rightchild) {
            stack.push(curr_node.rightchild);
        }
        if (curr_node.leftchild) {
            curr_node = curr_node.leftchild;
        } else {
            curr_node = stack.pop();
        }
    }
}