算法题解---栈类总结

100 阅读8分钟

📖前言

最近刷了几道栈的题目,觉得很深刻,想写个题解,想通过题解对自己的思路再一次总结,并且加深映像,准备通过发布到掘金来记录,由于笔者之前没有怎么写过算法题的总结,这是第一篇正式的对算法题的总结,所以有很多地方写的可能没有那么好,望多多海涵!



本文主要是以JavaScript的代码为主,其它代码就不过多介绍了,通过分享三道题对栈类的算法题进行总结



JavaScript栈的实现

实际上,JavaScript不像其它语言一样,直接提供了栈的实现,JS的栈的实现实际上是通过数组来进行的!

看了mdn发现,数组有push方法和pop方法可以实现入栈和出栈,还可以通过下标[length-1]来获取 栈顶元素

const animals = ["pigs", "goats", "sheep"];

const count = animals.push("cows"); //入栈 返回新的栈长度

console.log(count) //4

const value = animals.pop() //出栈 返回栈顶的值

console.log(value) //cows

console.log(animals[animals.length - 1]); //获取栈顶元素 此时是sheep


有效的括号

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

image.png

image.png

首先,题目给了三个有效的条件,那咱们就把无效的字符串的情况罗列出来。

    //1. {]
    //2. {}}}
    //3. ))
    //4. ()(((
    //5. (
    //6. }

这六种就是全部的不符合的情况,我们需要指定算法将这六种情况全部排除:

对s进行遍历:

1.若是[、{、(的话,则入栈

2.若是]、}、)的话,则进行分类讨论:

对于]

如果如果栈顶此时是[,那就pop()栈顶的[然后继续。

  • 栈顶此时不是[,则直接返回false(排除了1、2、3、6这种情况)。因为我们发现如果栈顶不是[,栈顶可能为空,那就是2、3、6这种情况,此时入栈]后,这个s一定是非法的,因为栈的特点:先进后出,后面没有人能把]给消掉,因此无论如何]入栈了之后就一定会存在,导致闭不合,从而无效。

  • 栈顶也可能为除了{或者,这个时候与入栈的]不匹配,此时入栈],同样的,由于后面没有人能把]给消掉,导致]会一直存在,导致闭不合。

对于})也是同样的,只有栈顶此时是对应的可以消除的符号{(,咱们才能消除,然后继续,否则和上面的情况一样,直接返回false。

咱们在初始为栈顶设置一个与题目不相干的0,作为初始栈顶

最后如果发现咱们的栈顶是0的,说明所有的入栈括号都能全部消除了,符合规则,返回true。

如果咱们的栈顶不为空,说明有括号没对应的括号匹配从而消除,此时返回false(情况4,5)

具体代码实现如下:

const stack = [];  //初始化栈  
stack.push(0);  //初始栈顶为0

//遍历字符串
for(e of s)
{
    //如果是左半边符号,则直接入栈
    if(e === '[' || e === '{' || e === '(')
    {
        stack.push(e);
    }
    //如果是右半边符号,进行分类讨论判断

    else if(e === ']')
    {
        //仅有对应着左半边的符号,才能消除并且继续
        if(stack[stack.length - 1] === '[')
        stack.pop();
        else
        {return false;}
    }

    else if(e === '}')
    {
        if(stack[stack.length - 1] === '{')
        stack.pop();
        else
        {return false;}
    }

    else if(e === ')')
    {
        if(stack[stack.length - 1] === '(')
        stack.pop();
        else
        {return false;}
    }
}

//当全部消除完了,符合题意
if(stack[stack.length - 1] === 0)
return true;
//否则,会有未消除的左符号
else
return false;

这道题主要是让我们熟悉一下栈,是一道很经典的入门题了。




最小栈

image.png

image.png

这道题让我们实现一个栈,通过观察发现其它的步骤都能够很轻松的实现,问题是getMin获取堆栈中的最小元素,并且还要以常数时间检索到最小元素。

一开始我在做这道题的时候,看到“常数”,“检索”,立马联想到了哈希表,可是哈希表是无序的,后面想了下结合链表,但是发现插入的时候找到最小值可能时间复杂度不止O1,所以这两者方式看来都是不可行的,我看了题解发现给出了 一个我第一次见的做法---辅助栈,不得不说第一次做这个题如果能够想到辅助栈,我觉得是真NB!

我们发现,在入栈元素a时,栈里有其它的元素b,c,d,之后不论经过了哪些操作,在a被弹出来之前,b,c,d一定不会被弹出来,假设此时有一个最小值,那么在a被弹出来之前,a作为栈顶时的最小值永远不会发生变化。

所以我们可以维护一个最小值的栈min_stack,我们可以巧妙地发现,当每个元素入栈时,可以通过比较当前元素和上一个状态的最小值,来O1地更新min_stack,将每一个元素入栈时的状态对应的最小值 入栈到 这个min_stack,从而我们就可以随时查找每个状态的最小值了,只需要取这个min_stack的栈顶就行了,取出来也是O1的时间复杂度。

接下来是代码部分:

var MinStack = function() {
    //初始化栈
    this.x_stack = [];
    //初始化最小值栈,栈顶设置无限大的元素,使得第一次就能够更新最小值
    this.min_stack = [Infinity];
};

MinStack.prototype.push = function(x) {
    //入栈
    this.x_stack.push(x);
    //维护这个状态的最小值
    this.min_stack.push(Math.min(this.min_stack[this.min_stack.length - 1], x));
};

MinStack.prototype.pop = function() {
    //出栈
    this.x_stack.pop();
    //当前状态的最小值可能会发生变化,更新
    this.min_stack.pop();
};

MinStack.prototype.top = function() {
    //正常获取栈顶
    return this.x_stack[this.x_stack.length - 1];
};

MinStack.prototype.getMin = function() {
    //获取我们维护的最小值
    return this.min_stack[this.min_stack.length - 1];
};

很巧妙地利用好了入栈时可以更新最小值,并且最小值放入栈中与原栈同步,实现了O1的时间复杂度来进行最小值的查找




字符串解码

image.png

image.png

我认为这道题是最难的一道,看到题目时会联想到栈,但怎么用好栈在这道题上才是最关键的。

题目的这种格式,我们一般都会从里往外看,脑海里先构造[]里面的内容,然后再往外一层一层构造出完整的结果,但是咱们遍历的话一般是从外往里构造,从左往右,越里面的越先构造,反而外面的后构造,那肯定是栈的先进后出了,后进先出了。

咱们用脑子从左到右模拟一遍过程,发现每次遇到]时才是我们结果的构造,而每次遇到]时,我们就会想要获得上一个[旁边的数字,和上一个[和这一个]里面的结果。

如果没有嵌套结构的话,那咱们就直接用一个number变量记录数字,用一个res变量记录结果就好了,反正每次都只有一个,但问题就是咱们题目里有时候会出现嵌套结构。

于是就不能使用单个变量了,因为我们需要获取当前的[]外面的信息了,我们通过观察发现这种嵌套结构可以很好地用栈来表示,于是我们就准备分别用两个栈来维护每一层[]的number和每一层[]的res,分别是number栈和res栈。

具体在嵌套时,我们模拟一遍发现,此时的当前的[]需要获取最近的nunber,也就是number栈的栈顶,还需要最近的res结果,使得当前的res = res * number + last_res,这样子直到字符串结束,刚好此时的res正是我们从内向外迭代的最终结果!

接下来是具体代码:

/**
 * @param {string} s
 * @return {string}
 */
var decodeString = function(s) {
    const res_stack = [];
    const number_stack = [];
    let res = "";
    //1.遍历
    
    //3.遇到数字 直接放入数字的栈
    //4.遇到[ 放入res栈 并且清空 ; 数字放入,并且清0
    //5. 遇到] res * 取出的数字 + 前一个res 赋值给res
    let number = 0;
    for (char of s)
    {
        if(!isNaN(char))
        {
            //遇到数字,进行处理
            number = number*10 + Number(char);
        }
        else if(char == '[')
        {
            //遇到 [
            //res放入栈,并且清空; number放入栈,并且清0
            res_stack.push(res);
            res = "";
            number_stack.push(number);
            number = 0;
        }
        else if(char == ']')
        {
            //遇到 ] 
            //更新当前res即可,并且pop栈
            let number = number_stack[number_stack.length - 1];
            let last_res = res_stack[res_stack.length - 1];
            res = last_res + res.repeat(number);
            number_stack.pop();
            res_stack.pop()
        }
        else{
            //普通字符append
            res += char;
        }    
    }
    return res;
};



🎯总结

总的来说,我们在做算法时,观察到题目中涉及到先进后出时,涉及到括号匹配等,我们就需要考虑栈的使用,需要仔细思考,可以先用脑子大概捋一遍流程,然后得出算法的实现!



🌇结尾

感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)

作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。