数据结构之----栈

116 阅读6分钟

栈是一种特殊的线性表,仅能够在栈顶进行操作,有着先进后出(后进先出)的特性,下面这张图展示了栈的工作特点:

栈有以下几个方法:

  • push 添加一个元素到栈顶(向桶里放入一个羽毛球)

  • pop 弹出栈顶元素(从桶里拿出一个羽毛球)

  • top 返回栈顶元素,注意,不是弹出(看一眼桶里最顶端的羽毛球,但是不拿)

  • isEmpty 判断栈是否为空(看看羽毛球是不是都用完了)

  • size 返回栈里元素的个数(数一下桶里还有多少羽毛球)

  • clear 清空栈(把桶里的羽毛球都倒出来扔掉)

    function Stack() { var items = []; // 使用数组存储数据

    // push方法向栈里压入一个元素
    this.push = function(item){
        items.push(item);
    };
    
    // pop方法把栈顶的元素弹出
    this.pop = function(){
        return items.pop();
    };
    
    // top 方法返回栈顶元素
    this.top = function(){
        return items[items.length - 1];
    };
    
    // isEmpty返回栈是否为空
    this.isEmpty = function(){
        return items.length == 0;
    };
    
    // size方法返回栈的大小
    this.size = function(){
        return items.length;
    };
    
    // clear 清空栈
    this.clear = function(){
        items = []
    }
    

    }

1 合法括号

下面的字符串中包含小括号,请编写一个函数判断字符串中的括号是否合法,所谓合法,就是括号成对出现

sdf(ds(ew(we)rw)rwqq)qwewe   合法
(sd(qwqw)sd(sd))             合法
()()sd()(sd()fw))(           不合法

1 遇到左括号,就把左括号压如栈中遇到右括号,判断栈是否为空,
2 为空说明没有左括号与之对应,缺少左括号,字符串括号不合法,
3 如果栈不为空,则把栈顶元素移除,这对括号抵消掉了
4 当for循环结束之后,如果栈是空的,就说明所有的左右括号都抵消掉了,如果栈里还有元素,
5 则说明缺少右括号,字符串括号不合法。
function is_leagl_brackets(string){
    var stack = new Stack();
    for(var i=0; i<string.length; i++ ){
        var item = string[i];
        if(item == "("){
            // 将左括号压入栈
            stack.push(item);
        }else if (item==")"){
            // 如果为空,就说明没有左括号与之抵消
            if(stack.isEmpty()){
                return false;
            }else{
                // 将栈顶的元素弹出
                stack.pop();
            }
        }

    }
    return stack.size() == 0;
};

console.log(is_leagl_brackets("()()))"));
console.log(is_leagl_brackets("sdf(ds(ew(we)rw)rwqq)qwewe"));
console.log(is_leagl_brackets("()()sd()(sd()fw))("));

2 计算逆波兰表达式

 题目要求

逆波兰表达式,也叫后缀表达式,它将复杂表达式转换为可以依靠简单的操作得到计算结果的表达式,例如 (a+b)*(c+d)转换为ab+cd+*。
示例:

["4", "13", "5", "/", "+"] 等价于(4 + (13 / 5)) = 6
["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"] 等价于((10 * (6 / ((9
 + 3) * -11))) + 17) + 5

["4", "13", "5", "/", "+"] 就是一个数组,在数组层面上思考这个问题,遇到 / 时,
把135 拿出来计算,然后把135 删除并把结果放入到4的后面,天哪,
太复杂了,太笨拙了,我已经无法继续思考了。如果是使用栈来解决这个问题,一切就那么的自然而简单,使用for循环遍历数组,对每一个元素做如下操作:如果元素不是 + - * / 中的某一个,就压入栈中如果元素是 + - * / 中的某一个,则从栈里连续弹出两个元素
,并对这两个元素进行计算,将计算结果压入栈中for循环结束之后,栈里只有一个元素,这个元素就是整个表达式的计算结果function calc_exp(exp){
    var stack = new Stack();
    for(var i = 0; i < exp.length;i++){
        var item = exp[i];

        if(["+", "-", "*", "/"].indexOf(item) >= 0){
            // 从栈顶弹出两个元素
            var value_1 = stack.pop();
            var value_2 = stack.pop();
            // 拼成表达式
            var exp_str = value_2 + item + value_1;
            // 计算并取整
            var res = parseInt(eval(exp_str));
            // 将计算结果压如栈
            stack.push(res.toString());
        }else{
            stack.push(item);
        }
    }
    // 表达式如果是正确的,最终,栈里还有一个元素,且正是表达式的计算结果
    return stack.pop();
};


var exp_1 = ["4", "13", "5", "/", "+"];
var exp_2 = ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"];
console.log(calc_exp(exp_1));
console.log(calc_exp(exp_2));

3 实现一个有min方法的栈

实现一个栈,除了常见的push,pop方法以外,提供一个min方法,返回栈里最小的元素,且时间复杂度为o(1)

0. 使用两个栈来存储数据,其中一个命名为data_stack,专门用来存储数据,另一个命名为. min_stack,专门用来存储栈里最小的数据。

  1. 我们要实现的是一个栈,除了常规的方法,还要有一个min方法,data_stack就是专门为常规方法而存在的,min_stack就是为了这个min方法而存在的。

  2. 编程思想里有一个分而治之的思想,简单来说,就是分开想,分开处理。那么我们现在考虑data_stack,这个时候别管min方法,你就只关心data_stack,它就是一个普通的栈啊,没什么特别的,一个简单的栈你还不会么,就是push,pop那些方法,正常实现就可以了。

    data_stack处理完了以后,再考虑min_stack,这个时候,你就别想data_stack了,只关心min_stack,它要存储栈里的最小值,我们先考虑边界情况,如果min_stack为空,这个时候,如果push进来一个数据,那这个数据一定是最小的,所以此时,直接放入min_stack即可。如果min_stack不为空,这个时候它的栈顶不正是栈的最小元素么,如果push进来的元素比栈顶元素还小,放入min_stack就好了,这样,min_stack的栈顶始终都是栈里的最小值。

    function MinStack(){ var data_stack = new Stack(); // 普通的栈 var min_stack = new Stack(); // 专门存储最小值

    // push的时候,两个栈都要操作
    this.push = function(item){
        data_stack.push(item);    // data_stack是常规栈,常规操作即可
    
        // 如果min_stack为空,直接放入,如果item小于min_stack栈顶元素,放入其中
        // 这样做的目的,是保证min_stack的栈顶始终保存栈的最小值
        if(min_stack.isEmpty() || item < min_stack.top()){
            min_stack.push(item);
        }else{
            // 如果item大于等于栈顶元素,把min_stack的栈顶元素再放入一次
            // min_stack的元素个数要和data_stack 保持一致
            min_stack.push(min_stack.top());
        }
    
    };
    
    // pop的时候,两个栈都pop
    this.pop = function(){
        min_stack.pop();
        return data_stack.pop();
    };
    
    // 直接取栈顶的元素
    this.min = function(){
        return min_stack.top();
    };
    

    };

    minstack = new MinStack();

    minstack.push(3); minstack.push(6); minstack.push(8); console.log(minstack.min()); minstack.push(2); console.log(minstack.min());

4 使用栈,完成中序表达式转后续表达式

定义数组postfix_lst,用于存储后缀表达式,遍历中缀表达式,对每一个遍历到的元素做如处理:
1、如果是数字,直接放入到postfix_lst中
2、遇到左括号入栈
3、遇到右括号,把栈顶元素弹出并放入到postfix_lst中,直到遇到左括号,最后弹出左括号
4、遇到运算符,把栈顶的运算符弹出,直到栈顶的运算符优先级小于当前运算符, 把弹出的运算符加入到postfix_lst,当前的运算符入栈
5、for循环结束后, 栈里可能还有元素,都弹出放入到postfix_lst中

输入:["12","+", "3"]
输出:["12","3","+"]

输入:["(","1","+","(","4","+","5","+","3",")",
"-","3",")","+","(","9","+","8",")"]
输出:[ '1', '4', '5', '+', '3', '+', '+', '3', '-', '9', '8', '+', '+' ]

输入:['(', '1', '+', '(', '4', '+', '5', '+', '3', 
')', '/', '4', '-', '3', ')', '+', '(', '6', '+', '8', ')', '*', '3']
输出:['1', '4', '5', '+', '3', '+', '4', 
'/', '+', '3', '-', '6', '8', '+', '3', '*', '+']

1、如果是数字,直接放入到postfix_lst中2、遇到左括号入栈3、遇到右括号,把栈顶元素弹出并放入到postfix_lst中,直到遇到左括号,最后弹出左括号4、遇到运算符,把栈顶的运算符弹出,直到栈顶的运算符优先级小于当前运算符,
 把弹出的运算符加入到postfix_lst,当前的运算符入栈5for循环结束后, 栈里可能还有元素,都弹出放入到postfix_lst中

// 定义运算符的优先级
var priority_map = {
    "+": 1,
    "-": 1,
    "*": 2,
    "/": 2};

function infix_exp_2_postfix_exp(exp){
    var stack = new Stack();

    var postfix_lst = [];
    for(var i = 0;i<exp.length;i++){
        var item = exp[i];
        // 如果是数字,直接放入到postfix_lst中
        if(!isNaN(item)){
            postfix_lst.push(item);
        }else if (item == "("){
            // 遇到左括号入栈
            stack.push(item);
        }else if (item == ")"){
            // 遇到右括号,把栈顶元素弹出,直到遇到左括号
            while(stack.top() != "("){
                postfix_lst.push(stack.pop());
            }
            stack.pop();   // 左括号出栈
        }else{
            // 遇到运算符,把栈顶的运算符弹出,直到栈顶的运算符优先级小于当前运算符
            while(!stack.isEmpty() && ["+", "-", "*", "/"].indexOf(stack.top()) >= 0 && priority_map[stack.top()] >= priority_map[item]){
                // 把弹出的运算符加入到postfix_lst
                postfix_lst.push(stack.pop());
            }
            // 当前的运算符入栈
            stack.push(item);
        }

    }

    // for循环结束后, 栈里可能还有元素,都弹出放入到postfix_lst中
    while(!stack.isEmpty()) {
        postfix_lst.push(stack.pop())
    }

    return postfix_lst
};


// 12+3
console.log(infix_exp_2_postfix_exp(["12","+", "3"]))
// 2-3+2
console.log(infix_exp_2_postfix_exp(["2","-", "3", "+", "2"]))
// (1+(4+5+3)-3)+(9+8)
var exp = ["(","1","+","(","4","+","5","+","3",")","-","3",")","+","(","9","+","8",")"];
console.log(infix_exp_2_postfix_exp(exp))

// (1+(4+5+3)/4-3)+(6+8)*3
var exp = ['(', '1', '+', '(', '4', '+', '5', '+', '3', ')', '/', '4', '-', '3', ')', '+', '(', '6', '+', '8', ')', '*', '3']
console.log(infix_exp_2_postfix_exp(exp))

console.log(infix_exp_2_postfix_exp(["12","+", "3","*", "5"]))
console.log(infix_exp_2_postfix_exp(["12","*", "3","+", "5"]))
1、只有一个运算符
中缀: 1 + 2
后缀: 1 2 + 

后缀表达式,数值在前,操作符在后,因此,遇到数值时直接放入到后缀表达式中。将操作符放入栈中,等到中缀表达式遍历结束后,将栈里的操作符弹出放入到后缀表达式中

2、多个运算符,栈顶操作符优先级和当前运算符相同

多个运算符,似乎也可以像第一步推理中那样操作,但实际不行

中缀: 1 + 2 - 3
后缀: 1 2 + 3 -

每次遇到操作符时,如果栈里有操作符,说明前面有需要计算的数值,且计算的操作符就在栈顶,应该弹出放入到后缀表达式中,如果都等到中缀表达式结束再弹出,就会变成 1 2 3 + -

就本示例而言,遇到减号时,后缀表达式里是 1 2 ,栈里是+,1和2需要计算,进行计算的操作符就在栈顶,因此需要弹出,放入到后缀表达式,之后减号入栈,中缀表达式遍历结束后,后缀表达式是 1 2 + 3, 栈里是-,将栈里的操作符去全部弹出,放入到后缀表达式,最终结果为 1 2 + 3 -.

3、多个运算符,栈顶操作符优先级大于当前运算符相同
中缀: 1 * 2 + 3
后缀: 1 2 * 3 +

栈顶运算符优先级高,和第2步的分析一致,前面有需要计算的数值,应该弹出放入到后缀表达式中

4、多个运算符,栈顶操作符优先级小于当前运算符相同
中缀: 1 + 2 * 3 
后缀: 1 2 3 * +

栈顶运算符优先级低,说明还不能进行计算,要继续压栈,压栈后,高优先级操作符在栈顶,出栈的时候先出,保证2*3先被计算

5、有括号的情况

括号里的的表达式可以视为一个独立的中缀表达式,因此,前面4步的分析都适用,但是括号里的中缀表达式在一个更大的表达式中,因此需要与其他部分分隔,分隔的方法就是遇到小括号后压栈,此后的操作遵循前面的推理逻辑,当遇到右括号时,说明括号内的表达式结束了,根据第一步的分析,应该把所有属于这个括号内表达式的操作符都弹出来放入到后缀表达式,最后一步弹出左括号。