栈(待完成续篇:单调栈)

前言

前面是复习完了线性表与线性表的链式存储结构,包括:单链表,循环链表,双向链表。当然啦,如果前两节没细看也莫得关系,因为这一节和前边关联不是特别大。我说的关联是应用层面的,不是实现层面的哈。
这一节复习的是栈和队列,学过计算机系统的话应该对栈已经有很深的理解啦。
cpp选手这儿就不着重讲如何实现这两种数据结构了哈,直奔主题利用头文件,即这两行代码引入数据结构即可:

#include<stack>
#include<queue>
#include<deque> // 双向队列

当然啦,如果想自己实现也挺简单的哈,可以参考我在第一篇文章推荐的教材或者自己另寻他法。

# 一、学习目标:

掌握栈和队列的基本操作,善于从题目中判断出是否用到此种数据结构(即能应用解决实际问题)


栈的基本操作

最开始当然是如何建立一个空栈啦,这样实现的:

stack<int> s; // 可以为任意Element Type 

通过上面一行代码一个栈就建立好啦,那么它包含了一些列操作,下图所示:
在这里插入图片描述
其实这些操作都挺简单的哈。我们一个个来看!
首先是size和empty这一对相辅相成的属性,这些都是英语本身的意思,即栈空间大小和是否空。第一个会返回int型整数,第二个会返回bool型。
示例如下:
在这里插入图片描述
一开始为空因此它是空的,为true。空间大小也为0。当我们装入一个元素1后,后面的输出情况就不同。
数据结构最重要的当然就是对数据的管理,数据进出情况。
在栈中,我们用push往栈里塞入元素,pop从栈中弹出元素。调用push时需要传入参数(即想要插入的元素),pop时无需参数且无返回值来看看Debug:
在这里插入图片描述
可以看到我们传入了元素0, 1, 2。当我们执行弹出的时候最后一个进入的2最先被弹出了。实际上这就是我们所说的“后进先出”的结构。即最后进入栈的会率先被弹出。所以栈是一种层叠式的结构,最先进入的会在最底层随后会一层层增高。最后进入的元素会在最高层。因此,诞生了top属性,无参数,会返回最后进入的值。
如下所示:
在这里插入图片描述
应该是不难理解的,就是最后进入的在最顶部然后将其输出。
还有emplace方法,你可以简单粗暴地认为其与push是一样的,当然想要深究的话可以看这儿emplace与push区别,或者看stackoverflow的Stack Overflow版本
最后就是swap方法的理解。
注意:swap交换的不是一个栈内的不同位置的元素,swap做的是交换两个相同类型的栈。如下所示:
在这里插入图片描述
不难看出p1,p2成功做了交换操作。

栈的应用范围也是很广的。书本有许多例子但就不一一讲解了,比较经典的有括号匹配问题。
问题很简单就是要算法设计者判断某一符号串是否合法。“(]”这样不匹配显然是不合法的。那如何思考此问题呢?
有些人会用“急切程度”来形容此问题,也就是说越晚遇到的左号(即’(’,,’[’,’{’)需要为其配对的程度是越迫切的。这个应该很好理解!
那么从这儿就可以衍生出一种思想也就是说越晚遇到的应该越早为其匹配,等等,栈不就是这么一种后进先出的数据结构嘛!也就是说我们一旦遇到左号即将其压入栈,然后遇到右号(即与左号相对)就检查其与栈顶的左号是否匹配。如匹配,将左号弹出,否则直接是不成立的符号串。最后整个串处理完毕后如果栈还有多余的左号,抑或是当出现右号却又栈已经空了都是不满足的。只有是刚好处理完串的最后一个元素的同时栈为空才是满足的。
LeetCode符号匹配,代码如下:

class Solution {
public:
    bool isValid(string s) {
        if(s[0] == ')' || s[0] == '}' || s[0] == ']')
            return false;
        else{
            int l = s.length();
            stack<char> st;
            st.push(s[0]);
            for(int i = 1 ; i < l ; i ++){
                if(s[i] == '{' || s[i] == '[' || s[i] == '(')
                    st.push(s[i]);
                else{
                    if(st.size() == 0)
                        return false;
                    if(s[i] == '}' && st.top() == '{')
                        st.pop();
                    else if(s[i] == ']' && st.top() == '[')
                        st.pop();
                    else if(s[i] == ')' && st.top() == '(')
                        st.pop();
                    else
                        return false;
                }
            }
            if(st.size() == 0)
                return true;
            return false;
        }
    }
};

课本上还有比较简单的行编辑问题和进阶版的迷宫问题,但由于未找到合适的题目就直接筛实战题吧。
首先是也是一道“括号题”,题目要求如下所示:在这里插入图片描述
我的想法也是很简单,题目反过来的意思就是要我们保留内层的括号。如何判断在最外层?用到的是栈,如果一个括号在栈底那么其就是最外层的,反之不然。
代码解读是这样的:
如果我们遇到一个左括号,那么如果其是最外层的,那么此时栈一定为空!
因为最外层的左括号其左边整个串要么为空,要么是一个完整的合法串。第一种情况的话栈未存放过任何元素,第二种情况的话栈pop到空为止。所以无论如何,只要遇到左括号且栈为空那么此左括号不予以保留,否则的话就加入结果串当中。
如果我们遇到的一个右括号,只要此时栈的大小为1就可以判断此右括号必然是与最外层的左括号相匹配的。因为根据上述证明最外层的左括号永远是第一个加入栈的,即当出现最右层右括号时栈中已经只剩下一个左括号了。那么如果此时栈为1不加入,否则也将右括号相应加入结果串中。
最后返回结果串即可,效率也是蛮不错的:
在这里插入图片描述
栈的题目就和括号作伴了吧,下一道题长这样的:
在这里插入图片描述
给人的感觉除了熟悉还是熟悉,和括号匹配是同一类问题。
这一题的思路也比较容易:
遍历字符串,如遇左括号‘(’就将其压入栈中。当遇到右括号时我们需要对栈顶做一个判断,如果此时栈顶就是左括号本身,那么就是刚好是一对括号的“()”情况。此时弹出左括号(即栈顶元素)同时压入一个分数‘1’表示得到了一分。
但还存在的一种情况即栈顶并非左括号,而是存在了一个分数。那么此时我们需要将这些分数都提取出来相加,不断pop直到遇到了左括号,然后将这些分数乘上2后,弹出匹配的那个左括号,压入最新的分数。
最后遍历完毕后,我们就对栈进行遍历,将里面所有的分数相加。
(说起来可能略微生涩,看代码就很好懂哒)
这里要注意的一点就是,我们用值0来替代左括号起到模拟的效果。

因为假如我们声明char类型的栈,那么对于一个个位以上分数其保存起来及其麻烦,因此用到的实际上还是int类型的栈。代码如下:

class Solution {
public:
    int scoreOfParentheses(string S) {
        int score = 0;
        stack<int> p;
        for(int i = 0 ; i < S.length() ; i ++){
            if(S[i] == '('){
                p.push(0);
            }
            else{
                int cnt = 0;
                if(p.top() == 0){
                    p.pop();
                    cnt = 1;
                }
                else{
                    while(p.top() != 0){
                        cnt += p.top();
                        p.pop();
                    }
                    p.pop();
                    cnt *= 2;
                }
                p.push(cnt);
            }
        }
        while(!p.empty()){
            score += (p.top());
            p.pop();
        }
        return score;
    }
};

有的同学可能会想最后直接提取栈顶不就是总得分了吗为啥还要遍历?
这里只考虑了括号嵌套的情况,即第一对括号包含了其他所有。但想一想假如数据是“()()”的话就不成立了哈。

在这里插入图片描述
最后一道困难题,是一道看起来很适合暴力法的题,长这样:
在这里插入图片描述
例题答案已给出,即高度为5和6的两根柱子,得到的答案就是2*5=10。
暴力代码也已给出,对一个柱子,对其往左右分别遍历,求得宽高后不断和最大面积进行比较,更新最大值即可。
等啊等,等啊等,终究等来了残酷的现实。
在这里插入图片描述
看到已经卡过了87个点是不是给了莫大的信心呢!不就是优化个O(n²)的算法嘛!先仔细想想自己的暴力算法缺陷何在?
没错!最大的劣势就是大量的重复遍历,对每个元素都去前后找,计算机表示他累了,如何为其减负?
这里涉及了一种叫做“单调栈”的算法思想,其实之前刷过我也忘了,于是重温了一哈。其实LeetCode里“栈”篇许多困难题都是单调栈的应用,我想着一时写不下那么多东西也没啥很好的构思。准备到时候专门写一篇单调栈的习题集。(应该是放在队列之后)
当然啦LeetCode里好多大佬的题解都很不错,我当时也是参考的。
附上链接:
单调栈题解
reference & emulate真的很重要!!!!!!
这里就直接先贴个图吧:
在这里插入图片描述

总之,栈作为一种辅助数据结构最重要的是如何从题目中发现栈的这种“后进先出”的思想从而去利用它。
但不能是盲目使用,应该做到用时间换取空间!即在开辟栈这一空间的情况下要优化时间复杂度。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值