经典面试题-栈结构扩展应用

文章详细介绍了如何利用栈解决LeetCode中的括号相关问题,包括判断有效括号序列、删除最外层括号以及移除无效括号。解题思路涉及栈的数据结构和遍历字符串,以及如何识别有效括号序列的原语。同时,提到了对于二叉树前序序列化的合法性判断方法。
摘要由CSDN通过智能技术生成

LeetCode 20 有效的括号

题目:

解题思路:利用栈,遍历字符串s,遇到左括号就将其入栈,遇到右括号就将其和栈顶元素配对。

代码:

class Solution {
public:
    bool isValid(string s) {
        stack<char> ss;
        for (int i=0; i < s.size(); i++) {
            switch (s[i]) {
                case '(':
                case '[':
                case '{': ss.push(s[i]); break;
                case ')': if (ss.empty() || ss.top() != '(') return false; ss.pop(); break;
                case ']': if (ss.empty() || ss.top() != '[') return false; ss.pop(); break;
                case '}': if (ss.empty() || ss.top() != '{') return false; ss.pop(); break;
            }
        }
        return ss.empty();
        //最后如果是空栈,才返回true。若这里不是空栈,证明左括号没有被匹配,返回false。
    }
};

LeetCode 1021 删除最外层的括号

题目:

题目简述:这道题让我们做两件事。

第一件事是先把一个括号序列分解成独立的几个部分。如:(()())(())分解成(()())和(())

第二件事就是把之前分解出来的每个部分的最外层的括号去掉。

如:(()())去掉最外层括号变成()(),同理(())变成()。

去掉最外层的括号后,再把剩余的部分拼接在一起成为一个新的字符串并输出。

即()()和()拼接成一个字符串()()()并输出。

Q: 怎么拆分括号序列(第一件事)?理论

□ 题目原文对此描述的大意是:拆分括号序列s,s=P1+P2+P3+...+Pk,P1~Pk(Pi)都是非空的有效(合法)的括号字符串原语

s是由题目给定的,且题目给出的s一定是个有效(合法)的括号字符串。

■ 意思就是说将一个题目给定的字符串(s)拆分成多个独立的部分(题目称之为“原语”),比如s=P1+P2+P3+...+Pk,且这些原语必须都得是非空的有效(合法)的括号序列。

Q: 那么原语是什么?

□ 题目原文对原语的描述是:非空的有效(合法)括号序列s,无法将s拆分成s=A+B(A和B都是非空的有效括号序列),那么称s为原语

■ 意思就是说原语s是非空的且最小的有效(合法)括号序列。

这里最小的有效括号序列意思就是说这个有效括号序列不能再拆分了,若强行拆分的话,那么就一定会拆分出不合法的括号序列的。

例:括号序列 (),(()),(()()()),它们就是原语。

强行拆分原语(())的话,如 "("+"())","("+"()"+")","("+"("+")"+")","(("+"))","(()"+")"等等。无论怎么拆分都一定会拆分出不合法的括号序列,那么这个序列(())就是最小的有效括号序列,也就是原语。

反例:(())() 可以拆分成"(())"+"()",且(())和()都是有效(合法)的括号序列"(())"+"()",故序列 (())() 不是原语。

如何代码实现将括号序列(string)拆分成原语的操作?实操

原理和上面例题(leetcode 20)判断括号序列是否合法一样,可以利用来识别有效(合法)的括号序列。

从前往后遍历欲拆分的括号序列s(string)。遇到左括号将其入栈,遇到右括号则与栈顶元素配对(原理同上面题目leetcode 20)。

如图所示,当 i 遍历到索引3的时候,第一次栈空(初始状态的栈空不算),就可以知道此时我们遍历过了一个有效(合法)的括号序列,而且可以确定这个括号序列是一个原语

栈空这种方法只能用来确定有效(合法)序列,之所以可以肯定遍历到的是原语,是因为第一次栈空可以确定我们刚遍历过了一个最小的有效(合法)括号序列,故可以确定刚刚遍历过了一个原语。

此时,我们就知道了括号序列s的索引0~3上的4个字符组成的括号序列 (()) 是一个原语。

● 接下来要继续找后面的原语:

下一个原语的起始一定是紧接着上一个原语的末尾(除非括号序列s不合法)。因此我们先根据上一个原语的末尾(索引3),得到下一个原语的开头pre(索引4),pre = i(索引3) + 1; ,再从pre开始往后遍历括号序列s。(pre代表当前原语的起始位置。)

当 i 遍历到索引5的时候,又是栈空(第二次栈空了),此时我们可以确定又遍历到了有效(合法)括号序列。

接下来要注意:原语是从位置pre(索引4)开始到位置 i(索引5)结束的。

□ 索引4~5( 序列"()" )是一个原语,因为期间只经历了1次栈空,证明这是最小的有效括号序列,故能证明其是原语。(经历1次栈空就是1个原语。)

□ 易混淆:索引0~5( 序列"(())()" )并不是原语,因为它经历了2次栈空,证明从索引0~5期间已经遍历过了2个原语(经历n次栈空就表明已经遍历了n个原语),序列"(())()"可以拆分出来两个原语,"(())()"="(())"+"()" ,故其并最小的有效括号序列(原语)

  之后拆分第3~n个原语也是同理。在遍历完整个括号序列s后,s就拆分完成了(拆分成n个原语)。

tips: 这个题目规定括号序列里面只有一种括号(小括号 '(' 和 ')') ,那么就还可以借助左括号和右括号数量的差值代替栈空拆分原语(识别有效括号序列)。具体做法可以请参考此博客里面的例题1

如何实现把拆分出来原语的最外层括号去掉(第二件事)?

理论

原语最外层括号一定是它的第一个字符最后一个字符

按照这个思路,把原语的首字符和末尾字符略过,只拿到其剩下的中间部分即可。

        

实操

以图1(左)为例,原语"(())"的索引是0~3,那么其去掉最外层的括号,就直接让它的起始索引(0)加1,末尾索引(3)减1即可,去掉最外层括号后,剩余部分的索引就是1~2,即序列"()"。

同理,图二(右)的原语"()"索引是4~5,其去掉最外层括号后剩余部分的索引是5~4,就得到了 “”(空序列)

原语去掉最外层的括号后,如何把剩余的部分拼接在一起成为一个新的字符串并输出?

创建一个字符串str(string),把原语去掉最外层括号后的结果增补到str字符串尾部,最后返回str即可。

代码(优质):

class Solution {
public:
    string removeOuterParentheses(string S) {
        string ret; //存储结果的字符串。
        //第1件事:把括号字符串S分解成多个独立的部分(原语)。
        for (int i=0, pre=0, cnt=0; i < S.size(); i++) { 
            //cnt记录左括号和右括号的差值,pre记录当前括号序列的起始位置。
            //这代码并没有使用栈stack。而使用左括号这右括号数量的差值cnt来判断括号序列是否有效。
            if (S[i] == '(') cnt += 1;
            else cnt -= 1;
            if (cnt != 0) continue;
            cout << S.substr(pre, i-pre+1) << endl; //输出每一个被拆分出来的原语。(输出来看看)
            //substr:截取字符串S,第1个参数是从位置pre开始截取,第2个参数是截取了多少个字符。
            //这里字符串s从位置pre开始,往后数i-pre+1 个字符,这些字符所组成的字符串就是这次被拆分出来的独立的部分(原语)。
            ret += S.substr(pre + 1, i-pre+1 - 2); //第2件事:把一个原语(除了最外层括号)放入字符串ret里。
            //字符串s从位置pre+1开始,往后数i-pre+1-2 个字符,这些字符组成的字符串就是当前原语去掉最外层括号后的结果。
            //当前原语:pre往后数i-pre+1,可以得出,其去掉最外层括号后的结果是:pre+1往后数i-pre+1-2。
            pre = i+1; //更新下一个序列的起始位置是i+1。
        }
        return ret;
    }
};

LeetCode 1249 移除无效的括号

题目:

解题思路:

从前往后遍历给定的字符串s,并设置一个栈,遇到左括号则将其入栈,遇到右括号就能将栈顶的左括号抵消使其出栈。

若遇到右括号时,栈空无法抵消,那么这个右括号就是无效的,应当被删除。

遍历完字符串s后,若栈中还有左括号,那么这些剩余的左括号就是无效的,也应该被删除。

看到题目给出的字符串中只会有一种括号(小括号),那么可以不使用栈来解题,用左括号和右括号的差值来解题。(这种编程思想值得参考。但是这道题特殊,实际写出来的代码很低效率,故不推荐这种方法实现。)

用栈实现的代码,这里不给了。

用左括号和右括号的差值解题的代码,上面提到过,此代码实际写出来比较低效率,所以代码不给出。

LeetCode 331 验证二叉树的前序序列化

这道题在判断树的合法性方面有很大的启发作用。

题目:

题目简述:请判断题目给定的字符串可不可以成为某个二叉树的合法的前序遍历序列?可以返回true,否则返回false。

解题思路:(理论)


※ 首先,先解释一下这道题是如何判断一棵二叉树是否合法的(判断树也一样)。

以叶子结点为切入,逐层的移除这棵树的叶子节点。一层一层,从下往上移除叶子节点。最后若这棵树的所有结点都能被移除,场上没有任何结点的话,那么这个树就是合法的。若最后场上还有结点无法被移除,则这棵树不合法(这就不会是一棵树)。


再谈这道题,欲判断题目给定的序列s是否可以是某棵树的前序遍历序列(判断中序,后序,层序遍历序列也是同样的方法),操作与上面判断树是否合法同理。

先遍历序列s,目的是为了找到并去掉序列s里面的叶子结点。

然后重复此步骤,逐层去掉序列s里面的叶子结点。

若最后序列里面没有任何结点的话,那么这个序列s就可以是某棵树的前序遍历序列。

具体的操作见下面解析:

                      ↓↓↓

以题目给定的前序遍历序列 “ 9, 3, 4, #, #, 1, #, #, 2, #, 6, #, # ” 为例,通过观察可以知道,当遍历到连续的2个'#'的时候,就意味着这2个'#'前面的就是叶子结点(比如结点 4, 1, 6)。

将叶子结点和它后面的2个'#'一起看成一个'#'。

如把 4,'#','#' 三个字符看成一个字符 '#'1,'#','#' 看成 '#'6,'#','#' 看成 '#'

然后序列 “ 9, 3, 4, #, #, 1, #, #, 2, #, 6, #, # ” 就变成了 “ 9, 3, #, #, 2, #, # ” 。

再观察序列,发现 3,'#','#'2,'#','#' ,结点3和结点2又成为了新的叶子结点。然后再同上面步骤,把 3,'#','#' 看成 '#'2,'#','#' 看成 '#'

此时序列变成了 “ 9,#,# ” 。

然后再重复上面步骤,序列 “ 9,#,# ” 变成了序列 “ # ”。

如果最后给定的序列只剩下一个字符“#”,那么就证明这个给定的序列可以是某棵树的前序遍历序列。

实际操作:(代码的流程)

上面讲的都是用来参考的理论,实际的代码操作和上面所讲的理论步骤略有不同。

用栈来模拟递归

示例代码:

class Solution {
public:
    bool isValidSerialization(string preorder) {
        vector<string> s; //把vector当作栈(stack)来使用。
        //把题目给的preorder字符串,根据,(逗号字符)分割成多个小字符串,并存储入栈s里。
        for (int i=0, j=0; i < preorder.size(); i = j + 1) {
        //j用来指向,(逗号字符)的位置。i用来指向每一个(欲分割出来的)小字符串的起始位置。
            j = i; //j要从i开始往后寻找,(逗号字符)。
            while (j < preorder.size() && preorder[j] != ',') ++j; //让j指向,(逗号字符)。
            s.push_back(preorder.substr(i, j-i)); //从i开始,截取j-i个字符。(索引i~j-1)
            //i~j-1,就是被分割出来的小字符串,要么是个"#",要么是个由数字组成的字符串。

            //去掉s序列里面的叶子节点。
            int last = s.size() - 1; //last指向s的最后一个元素的位置。
            while (s.size() >= 3 && s[last] == "#" && s[last-1] == "#") { 
            //若发现 "X","#","#" ,则将其化成 "#"。("X"是由数字组成的字符串)
            /*
            * 这里只能用循环(while),不能用if。
            * 具体原因:if只能去掉了最底一层的叶子节点,while能逐层把所有叶子结点由下至上全部去掉。
            * 如序列 "X1","X2","#","#","X3","#","#" ,用循环(while)最后化成"#"。(正确√)
            * 但如果这里用if语句,那么就会化成 "X1","#","#" ,新产生的叶子结点"X1"就没有被去掉。(错误×)
            */
                s[last - 2] = "#"; //这里把"X","#","#"化成 "#"。
                s.pop_back();
                s.pop_back();
                last = s.size() - 1; //更新s末尾元素的位置。
            }
            //解决特殊情况。
            if (s.size() == 2 && s[0] == "#" && s[1] == "#") return false;
            //若最终s被化成 "#","#" ,那么一定不是对的。
        }

        return s.size() == 1 && s[0] == "#"; //若最后s只剩下一个"#",那么就是对的。
    }
};

优化后的代码(逻辑更加清晰):※标记的就是发生改动的代码。(只改动了2行代码)

class Solution {
public:
    bool isValidSerialization(string preorder) { //注:这里被※标记的代码是进行了改动的。
        vector<string> s; //把vector当作栈来使用。
        //把题目给的preorder字符串,根据,(逗号字符)分割成多个小字符串,并存储入栈s里。
        for (int i=0, j=0; i < preorder.size(); i = j + 1) {
        //j用来指向,(逗号字符)的位置。i用来指向每一个(欲分割出来的)小字符串的起始位置。
            j = i; //j要从i开始往后寻找,(逗号字符)。
            while (j < preorder.size() && preorder[j] != ',') ++j; //让j指向,(逗号字符)。
            s.push_back(preorder.substr(i, j-i)); //从i开始,截取j-i个字符。(索引i~j-1)

            //逐层去掉s序列里面的叶子节点。
            int last = s.size() - 1; //last指向s的最后一个元素的位置。
            while (s.size() >= 3 && s[last] == "#" && s[last-1] == "#" && s[last-2] != "#") {// ※
            //若发现 "X","#","#" ,则将其化成 "#"。("X"是由数字组成的字符串)
                s[last - 2] = "#";
                s.pop_back();
                s.pop_back();
                last = s.size() - 1; //更新s末尾元素的位置。
            }
            
            //下面这行代码可以没有,取而代之的是第14行代码↑↑↑(上面※标记)增加了一个条件 s[last-2]!="#" 。
            //if (s.size() == 2 && s[0] == "#" && s[1] == "#") return false; // ※ 注释掉此行代码
            //解决特殊情况: s里面是"#","#"的情况。
        }

        return s.size() == 1 && s[0] == "#"; //若最后s只剩下一个"#",那么就是对的,返回true。
    }
};

 

LeetCode 227 基本计算器 II

这道题就是表达式求值问题。

思路:把表达式拆分成两个部分:一部分只有操作数,另一部分只有运算符。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值