输入: "((())" 输出: 4
解题思路
- 本题可以使用栈来解决。我们遍历给定的字符串,使用一个栈来保存遇到的字符的下标。 当遇到左括号时,我们将其下标入栈。
- 当遇到右括号时,我们尝试从栈中弹出一个左括号的下标,表示匹配了一个括号对。
- 如果栈为空,则当前右括号没有匹配的左括号,我们将其下标入栈以作为新的起始点。
- 如果栈不为空,则计算当前有效括号的长度,即当前右括号的下标减去栈顶元素的值(栈顶元素表示上一个无法匹配的右括号的下标)
- 在遍历过程中,我们通过不断更新最大长度来记录最长有效括号子串的长度。
我的代码
class Solution { public: int longestValidParentheses(string s) { int maxans = 0; stack<int> stk; stk.push(-1); // 初始值,表示最后一个无法匹配的右括号的下标 for (int i = 0; i < s.length(); i++) { if (s[i] == '(') { stk.push(i); } else { stk.pop(); if (stk.empty()) { stk.push(i); } else { maxans = max(maxans, i - stk.top()); } } } return maxans; } };
在解决本题的代码中,我们使用了一个栈来保存不匹配的括号的下标。遍历字符串时,对于左括号,将其下标入栈;对于右括号,尝试弹出一个左括号的下标。通过计算当前右括号的下标与栈顶元素的差值来获得有效括号的长度,并不断更新最大长度。最后返回最长有效括号子串的长度。
📍682. 棒球比赛
问题描述
给定一个特殊赛制棒球比赛的记录操作列表 ops,其中每个操作代表本回合的得分情况。需要计算所有得分的总和。
操作规则如下:
- 整数 x:表示本回合获得分数为 x。
- +:表示本回合的得分是前两次得分的总和。保证记录此操作时前面总是存在两个有效的分数。
- D:表示本回合的得分是前一次得分的两倍。保证记录此操作时前面总是存在一个有效的分数。
- C:表示前一次得分无效,将其从记录中移除。保证记录此操作时前面总是存在一个有效的分数。
要求返回所有得分的总和。
示例
输入:ops = ["5","2","C","D","+"]
输出:30
解释:
- “5” - 记录加 5 ,记录现在是 [5]
- “2” - 记录加 2 ,记录现在是 [5, 2]
- “C” - 使前一次得分的记录无效并将其移除,记录现在是 [5]。
- “D” - 记录加 2 * 5 = 10 ,记录现在是 [5, 10]。
- “+” - 记录加 5 + 10 = 15 ,记录现在是 [5, 10, 15]。
- 所有得分的总和 5 + 10 + 15 = 30。
解题思路
使用一个整数变量 ret 来保存得分的总和,使用一个整数数组 points 来保存记录操作的得分。遍历给定的 ops 列表,根据不同的操作进行相应的处理。
- 对于整数操作,将其转换为整数并加到总和 ret 中,同时将该得分添加到数组 points 中。
- 对于 “+” 操作,取数组 points 中最后两个元素的和,并将其加到总和 ret 中,同时将该得分添加到数组 points 中。
- 对于 “D” 操作,取数组 points 中最后一个元素的两倍,并将其加到总和 ret 中,同时将该得分添加到数组 points 中。
- 对于 “C” 操作,将数组 points 中最后一个元素从总和 ret 中减去,并将其从数组 points 中移除。
最后返回得分的总和 ret 即可。
我的代码
class Solution { public: int calPoints(vector<string>& ops) { int ret = 0; vector<int> points; for (auto& op : ops) { int n = points.size(); switch (op[0]) { case '+': ret += points[n - 1] + points[n - 2]; points.push_back(points[n - 1] + points[n - 2]); break; case 'D': ret += 2 * points[n - 1]; points.push_back(2 * points[n - 1]); break; case 'C': ret -= points[n - 1]; points.pop_back(); break; default: ret += stoi(op); points.push_back(stoi(op)); break; } } return ret; } };
📍面试题 03.01. 三合一
问题描述
给定一个固定大小的栈,需要实现三个栈在同一个数组中共享内存。具体要求实现以下几个方法:
- push(stackNum, value): 将元素压入指定栈中;
- pop(stackNum): 从指定栈中弹出一个元素,并返回其值;
- isEmpty(stackNum): 判断指定栈是否为空;
- peek(stackNum): 返回指定栈的栈顶元素值。
构造函数会传入一个参数 stackSize,表示每个栈的大小。
解题思路
我们可以使用一个一维数组来模拟三个栈,使用额外的三个指针(spointer[0]、spointer[1]、spointer[2])来标记各个栈的栈顶位置。通过适当的操作,可以实现栈的压入、弹出、判空和取顶操作。
具体实现如下:
- 构造函数:创建一个大小为 stackSize * 3 的数组,并将栈顶指针初始化为各个栈的起始位置,即 spointer[0] = 0、spointer[1] = stackSize、spointer[2] = stackSize * 2;
- 压入元素:首先判断指定栈是否已满,即栈顶指针是否超出了当前栈的范围,若未满,则将元素压入栈顶,并更新栈顶指针;
- 弹出元素:首先判断指定栈是否为空,即栈顶指针是否在当前栈的起始位置上,若非空,则从栈顶弹出一个元素,并更新栈顶指针;
- 判空操作:判断指定栈是否为空,即栈顶指针是否在当前栈的起始位置上;
- 取顶操作:首先判断指定栈是否为空,若非空,则返回栈顶元素的值。
我的代码
class TripleInOne { private: vector<int> s; // 存储数据的数组 int stackSize; // 每个栈的大小 int spointer[3]; // 各个栈的栈顶指针 public: TripleInOne(int stackSize) { s = vector<int>(stackSize * 3, 0); // 创建大小为 stackSize * 3 的数组 this->stackSize = stackSize; spointer[0] = 0; // 栈0的栈顶指针初始化为0 spointer[1] = stackSize; // 栈1的栈顶指针初始化为stackSize spointer[2] = stackSize * 2; // 栈2的栈顶指针初始化为stackSize * 2 } void push(int stackNum, int value) { // 判断栈是否已满 if (spointer[stackNum] < (stackNum + 1) * stackSize) { s[spointer[stackNum]++] = value; // 将元素压入栈中,并更新栈顶指针 } } int pop(int stackNum) { int res = -1; // 默认返回-1表示栈为空 // 判断栈是否非空 if (spointer[stackNum] > stackNum * stackSize) { res = s[--spointer[stackNum]]; // 弹出一个元素,并更新栈顶指针 } return res; } int peek(int stackNum) { int res = -1; // 默认返回-1表示栈为空 // 判断栈是否非空 if (spointer[stackNum] > stackNum * stackSize) { res = s[spointer[stackNum] - 1]; // 返回栈顶元素的值 } return res; } bool isEmpty(int stackNum) { return spointer[stackNum] == stackNum * stackSize; // 判断栈是否为空 } };
📍stack基础知识
🎈stack的介绍
👨🚀小明:“这里我们先看一下文档是怎么说的。总结就是下面几点。”
- ✨stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
- ✨stack是作为容器适配器被实现的,这里在前情提要里讲过了,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
- ✨stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
- empty:判空操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
- ✨标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。
👨🚀小明给小星展示了一张表“它的常用函数有下面这些,每一个都是链接了文档的超链接,想了解更多就点它,后面接口说明有功能介绍”
🎈stack的常用函数
🎈stack的使用
👨🚀小明:“在前面介绍了函数的功能,是不是觉得很简单?(又想考验小星了)”
🧚小星:“就这?我幼儿园都能学会(叉腰)”
👨🚀小明:“好,那我就给你出几道题,你做做(呵,鱼儿上钩了)”
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
- MinStack() 初始化堆栈对象。
- void push(int val) 将元素val推入堆栈。
- void pop() 删除堆栈顶部的元素。
- int top() 获取堆栈顶部的元素。
- int getMin() 获取堆栈中的最小元素。
示例 1:
输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
提示:
-231 <= val <= 231 - 1
pop、top 和 getMin 操作总是在 非空栈 上调用
push, pop, top, and getMin最多被调用 3 * 104 次
接口是这样的:
class MinStack { public: MinStack() { } void push(int val) { } void pop() { } int top() { } int getMin() { } };
🧚小星陷入了思考:“又要是栈,又要在常数时间内检索最小值,常数时间内检索说明是有东西存着它的,诶,我可以另外定义一个栈来存它的最小值,对!只要从第一个数入栈开始,以后的每一个比前一个小的数就入栈,在出栈的时候就判断是否是当前最小值,是就一起出栈,不是,则保留最小值的那个栈就不出栈,嗯,没错,果然天才如我哈哈哈。”
写出代码如下:
class MinStack { public: MinStack() { } void push(int val) { use.push(val); if(rem.empty()||rem.top()>=val) { rem.push(val); } } void pop() { if(rem.top()==use.top()) { rem.pop(); } use.pop(); } int top() { return use.top(); } int getMin() { return rem.top(); } private: stack<int>use; stack<int>rem; };
过了!!!
👨🚀小明此时呆住:心想:“我去,这小子有点子天赋啊,我当年可没这么快想出来,不过为了不让他骄傲,得打击他一下,对,我绝不是腹黑”
于是说:“啧啧啧,这么简单的题还用想?我亲爱的蠢弟弟,居然用了十分钟,就我当年一层的水平吧~来看看下一题,这题可不能这么慢了嗷”
🧚小星心想“二哥这么厉害的么,看来我得努力了”,下题给他眼前一亮哼哼“
于是说:”欧克欧克,没问题!“
👨🚀小明给出了第二题。
第二题如下:
给你一个字符串数组 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 + * 也可以依据次序计算出正确结果。
适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
🧚小星这下有点懵了,计算逆波兰表示法 表示的算术表达式???,大写的问号好吧。
👨🚀小明看出来小星不会了说:”其实没你想的那么难,一个公式把你唬住了而已(摇摇头),
这道题的思路其实非常清晰,只需要使用栈来模拟整个计算过程即可。
具体来说,我们遍历 tokens 中的每个字符串,判断当前字符是操作符还是数字:
如果当前字符是数字,将其转换为整数并压入栈中;
否则,从栈顶弹出两个数字进行计算,然后把结果压入栈中。
最后,我们只需返回栈顶元素即可得到表达式的值。
需要注意的地方是,整数除法总是向零截断。
此外,如果 tokens 列表为空,则应直接返回0。
这不就解出来了吗?“
代码如下:
class Solution { public: int evalRPN(vector<string>& tokens) { long long res; stack<int> st1; for(auto e : tokens) { if(e=="+"||e=="-"||e=="*"||e=="/") { long long right=st1.top(); st1.pop(); long long left=st1.top(); st1.pop(); switch(e[0]) { case '+':st1.push(left+right); break; case '-':st1.push(left-right); break; case '*':st1.push((int)(left*right)); break; case '/':st1.push(left/right); break; } } else { st1.push(stoll(e)); } } return st1.top(); } };
🧚小星心想姜还是老的辣,但是嘴上还是不松口:”哦哦就是这样啊,我会了”