今日学习的文章和视频链接
https://leetcode.cn/problems/valid-parentheses/description/
https://programmercarl.com/0020.%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7.html
20 有效的括号
题目描述
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
提示:
1 <= s.length <= 104
s 仅由括号 '()[]{}' 组成
我遇到的问题
判断右括号后忘记pop
题目分析
括号匹配是使用栈解决的经典问题。
如果还记得编译原理的话,编译器在 词法分析的过程中处理括号、花括号等这个符号的逻辑,也是使用了栈这种数据结构。
由于栈结构的特殊性,非常适合做对称匹配类的题目。
首先要弄清楚,字符串里的括号不匹配有几种情况。
先来分析一下 这里有三种不匹配的情况,
- 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
- 括号没有多余,但是括号的类型没有匹配上
- 字符串里右方向的括号多余了,所以不匹配
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
那么什么时候说明左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。
但还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!
我的acm模式代码
#include <iostream>
#include <stack>
class Solution {
public:
bool isValid(std::string s) {
std::stack<char> st;
if (s.size() % 2 != 0) return false;
for (int i = 0; i < s.size(); i++) {
if (s[i] == '(') {
st.push(')');
}
else if(s[i] == '[') {
st.push(']');
}
else if(s[i] == '{') {
st.push('}');
}
else if(st.empty() || st.top() != s[i]) {
return false;
}
else {
st.pop();
}
}
return st.empty();
}
};
int main() {
std::string s = "(())[]{}";
Solution sol;
if(sol.isValid(s)) {
std::cout << "valid string" <<std::endl;
}
else {
std::cout << "unvalid string" << std::endl;
}
}
这段C++代码的目的是检查一个只包含圆括号、方括号和花括号的字符串是否有效。它用于判断输入的字符串是否含有有效的 {}
、()
和 []
配对和顺序。
时间复杂度分析:
-
线性时间复杂度 (O(n)):
isValid
函数遍历输入字符串s
的每个字符一次。因此,时间复杂度与字符串的长度成正比,导致线性时间复杂度为 O(n),其中 n 是字符串的长度。
空间复杂度分析:
-
线性空间复杂度 (O(n)): 空间复杂度主要由栈
st
的使用决定。在最坏的情况下(例如,一个开括号的字符串),栈可能会存储几乎每个字符,直到开始弹出为止。因此,空间复杂度也是线性的,O(n),与输入字符串的长度成正比。
总的来说,考虑到线性的时间和空间复杂度,这个函数对于合理长度的字符串来说是高效的。
1047. 删除字符串中的所有相邻重复项
题目分析
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
提示:
1 <= S.length <= 20000
S 仅由小写英文字母组成。
我看到题目后的想法
和上1题类似,用栈来存储,进行类似消消乐,最后逆序保存栈顶元素作为输出
题目分析
我们在删除相邻重复项的时候,其实就是要知道当前遍历的这个元素,我们在前一位是不是遍历过一样数值的元素,那么如何记录前面遍历过的元素呢?
所以就是用栈来存放,那么栈的目的,就是存放遍历过的元素,当遍历当前的这个元素的时候,去栈里看一下我们是不是遍历过相同数值的相邻元素。
从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒序的,所以再对字符串进行反转一下,就得到了最终的结果。
这道题目就像是我们玩过的游戏对对碰,如果相同的元素挨在一起就要消除。
可能我们在玩游戏的时候感觉理所当然应该消除,但程序又怎么知道该如何消除呢,特别是消除之后又有新的元素可能挨在一起。
此时游戏的后端逻辑就可以用一个栈来实现(我没有实际考察对对碰或者爱消除游戏的代码实现,仅从原理上进行推断)。
游戏开发可能使用栈结构,编程语言的一些功能实现也会使用栈结构,实现函数递归调用就需要栈,但不是每种编程语言都支持递归,例如:
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
相信大家应该遇到过一种错误就是栈溢出,系统输出的异常是Segmentation fault(当然不是所有的Segmentation fault 都是栈溢出导致的) ,如果你使用了递归,就要想一想是不是无限递归了,那么系统调用栈就会溢出。
而且在企业项目开发中,尽量不要使用递归!在项目比较大的时候,由于参数多,全局变量等等,使用递归很容易判断不充分return的条件,非常容易无限递归(或者递归层级过深),造成栈溢出错误(这种问题还不好排查!)
我的acm模式代码
#include <iostream>
#include <stack>
class Solution {
public:
std::string removeDuplicates(std::string s) {
std::stack<char> st;
for (int i = 0; i < s.size(); i++) {
if (st.empty() || st.top() != s[i]) {
st.push(s[i]);
}
else {
st.pop();
}
}
std::string res;
res.resize(st.size());
int n = st.size();
for (int i = st.size() - 1; i >= 0; i--) {
res[i] = st.top();
st.pop();
}
return res;
}
};
int main() {
std::string s = "abbaca";
Solution sol;
std::string res = sol.removeDuplicates(s);
std::cout << res <<std::endl;
}
我遇到的困难
自己写的时候没有进行res.resize.无法给res进行赋值
问题出在尝试向 std::string res
赋值的方式上。当使用 res[i] = st.top();
这行代码时,假设 res
是一个空字符串,实际上在尝试访问并修改一个不存在的字符位置。在 C++ 中,这种操作是不合法的,因为在访问未被初始化或分配的内存。
解决这个问题有几种方法:
-
使用
push_back
或+=
: 在遍历栈时,可以使用push_back
或+=
来向res
中添加字符。例如,res.push_back(st.top());
。 -
预先分配字符串大小: 另一种方法是,在填充
res
之前,先设置它的大小。可以在将字符弹出栈之前,使用std::string
的resize
方法来设置字符串的大小。
修改后的代码示例如下:
class Solution {
public:
std::string removeDuplicates(std::string s) {
std::stack<char> st;
for (char ch : s) {
if (st.empty() || st.top() != ch) {
st.push(ch);
}
else {
st.pop();
}
}
std::string res;
res.resize(st.size()); // 设置字符串大小
for (int i = st.size() - 1; i >= 0; i--) {
res[i] = st.top();
st.pop();
}
return res;
}
};
或者,使用 push_back
:
class Solution {
public:
std::string removeDuplicates(std::string s) {
std::stack<char> st;
for (char ch : s) {
if (st.empty() || st.top() != ch) {
st.push(ch);
}
else {
st.pop();
}
}
std::string res;
while (!st.empty()) {
res = st.top() + res; // 在字符串前面添加字符
st.pop();
}
return res;
}
};
这两种方法都能有效地解决无法给 res
赋值的问题。
逆波兰表达式求值
题目分析
给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。
请你计算该表达式。返回一个表示表达式值的整数。
注意:
有效的算符为 '+'、'-'、'*' 和 '/' 。
每个操作数(运算对象)都可以是一个整数或者另一个表达式。
两个整数之间的除法总是 向零截断 。
表达式中不含除零运算。
输入是一个根据逆波兰表示法表示的算术表达式。
答案及所有中间计算结果可以用 32 位 整数表示。
示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
提示:
1 <= tokens.length <= 104
tokens[i] 是一个算符("+"、"-"、"*" 或 "/"),或是在范围 [-200, 200] 内的一个整数
逆波兰表达式:
逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。
逆波兰表达式主要有以下两个优点:
去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
我的想法
定义两个整数变量,用来存储操作数
首先遍历将数字入栈,遇到第一个符号时,将前两个数字元素出栈进行符号运算,结果再入栈,
题目分析
其实逆波兰表达式相当于是二叉树中的后序遍历。 大家可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。
但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后序遍历的方式把二叉树序列化了,就可以了。
在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么这岂不就是一个相邻字符串消除的过程,和1047.删除字符串中的所有相邻重复项 (opens new window)中的对对碰游戏是不是就非常像了。
我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。
例如:4 + 13 / 5,这就是中缀表达式,计算机从左到右去扫描的话,扫到13,还要判断13后面是什么运算符,还要比较一下优先级,然后13还和后面的5做运算,做完运算之后,还要向前回退到 4 的位置,继续做加法,你说麻不麻烦!
那么将中缀表达式,转化为后缀表达式之后:[“4”, “13”, “5”, “/”, “+”] ,就不一样了,计算机可以利用栈来顺序处理,不需要考虑优先级了。也不用回退了, 所以后缀表达式对计算机来说是非常友好的。
可以说本题不仅仅是一道好题,也展现出计算机的思考方式。
在1970年代和1980年代,惠普在其所有台式和手持式计算器中都使用了RPN(后缀表达式),直到2020年代仍在某些模型中使用了RPN。文章来源:https://www.toymoban.com/news/detail-796297.html
我的acm代码
#include <iostream>
#include <vector>
#include <stack>
#include <string>
class Solution {
public:
int evalRPN(std::vector<std::string>& tokens) {
int a,res;
if (tokens.size() == 1) {
res = std::stoi(tokens[0]);
return res;
}
std::stack<std::string> st;
for (std::string s:tokens) {
if (s != "+" && s != "-" && s != "*" && s != "/") {
st.push(s);
}
else {
res = std::stoi(st.top());
st.pop();
a = std::stoi(st.top());
st.pop();
if (s == "+") {
res = a + res;
}
else if (s == "-") {
res = a - res;
}
else if (s == "*") {
res = a * res;
}
else if (s == "/") {
res = a / res;
}
st.push(std::to_string(res));
}
}
return res;
}
};
int main () {
// std::vector<std::string> tokens = {"2","1","+","3","*"};
std::vector<std::string> tokens = {"10","6","9","3","+","-11","*","/","*","17","+","5","+"};
Solution sol;
int res = sol.evalRPN(tokens);
std::cout << res << std::endl;
}
我遇到的困难
没有考虑到只有一个元素的情况,Leetcode上有一个实例没有通过,加上以下代码文章来源地址https://www.toymoban.com/news/detail-796297.html
if (tokens.size() == 1) {
res = std::stoi(tokens[0]);
return res;
}
到了这里,关于算法训练day11Leetcode20有效的括号1047删除字符串中所有相邻重复项150逆波兰表达式求值的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!