【算法训练营】栈合集(1) 剑指 Offer 31. 栈的压入、弹出序列 || 32. 最长有效括号 || 682. 棒球比赛 || 面试题 03.01. 三合一

简介: 【算法训练营】栈合集(1) 剑指 Offer 31. 栈的压入、弹出序列 || 32. 最长有效括号 || 682. 棒球比赛 || 面试题 03.01. 三合一


输入: "((())"
输出: 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])来标记各个栈的栈顶位置。通过适当的操作,可以实现栈的压入、弹出、判空和取顶操作。

具体实现如下:

  1. 构造函数:创建一个大小为 stackSize * 3 的数组,并将栈顶指针初始化为各个栈的起始位置,即 spointer[0] = 0、spointer[1] = stackSize、spointer[2] = stackSize * 2;
  2. 压入元素:首先判断指定栈是否已满,即栈顶指针是否超出了当前栈的范围,若未满,则将元素压入栈顶,并更新栈顶指针;
  3. 弹出元素:首先判断指定栈是否为空,即栈顶指针是否在当前栈的起始位置上,若非空,则从栈顶弹出一个元素,并更新栈顶指针;
  4. 判空操作:判断指定栈是否为空,即栈顶指针是否在当前栈的起始位置上;
  5. 取顶操作:首先判断指定栈是否为空,若非空,则返回栈顶元素的值。

我的代码

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文档

  1. ✨stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
  2. ✨stack是作为容器适配器被实现的,这里在前情提要里讲过了,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
  3. ✨stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
  • empty:判空操作
  • back:获取尾部元素操作
  • push_back:尾部插入元素操作
  • pop_back:尾部删除元素操作
  1. ✨标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque。

👨‍🚀小明给小星展示了一张表“它的常用函数有下面这些,每一个都是链接了文档的超链接,想了解更多就点它,后面接口说明有功能介绍”

🎈stack的常用函数

✨函数说明 ✨接口说明
✨stack() 构造空的栈
✨empty() 检测stack是否为空
✨size() 返回stack中元素的个数
✨top() 返回栈顶元素的引用
✨push() 将元素val压入stack中
✨pop() 将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();
    }
};

🧚小星心想姜还是老的辣,但是嘴上还是不松口:”哦哦就是这样啊,我会了”

相关文章
|
7天前
|
存储 算法 Java
leetcode算法题-有效的括号(简单)
【11月更文挑战第5天】本文介绍了 LeetCode 上“有效的括号”这道题的解法。题目要求判断一个只包含括号字符的字符串是否有效。有效字符串需满足左括号必须用相同类型的右括号闭合,并且左括号必须以正确的顺序闭合。解题思路是使用栈数据结构,遍历字符串时将左括号压入栈中,遇到右括号时检查栈顶元素是否匹配。最后根据栈是否为空来判断字符串中的括号是否有效。示例代码包括 Python 和 Java 版本。
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
55 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
29天前
|
算法 Java 数据库
美团面试:百亿级分片,如何设计基因算法?
40岁老架构师尼恩分享分库分表的基因算法设计,涵盖分片键选择、水平拆分策略及基因法优化查询效率等内容,助力面试者应对大厂技术面试,提高架构设计能力。
美团面试:百亿级分片,如何设计基因算法?
|
1月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
28 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
1月前
|
算法 前端开发 Java
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
这篇文章总结了单链表的常见面试题,并提供了详细的问题分析、思路分析以及Java代码实现,包括求单链表中有效节点的个数、查找单链表中的倒数第k个节点、单链表的反转以及从尾到头打印单链表等题目。
32 1
数据结构与算法学习四:单链表面试题,新浪、腾讯【有难度】、百度面试题
|
15天前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
23天前
|
机器学习/深度学习 算法 Java
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
|
29天前
|
算法 Java 数据库
美团面试:百亿级分片,如何设计基因算法?
40岁老架构师尼恩在读者群中分享了关于分库分表的基因算法设计,旨在帮助大家应对一线互联网企业的面试题。文章详细介绍了分库分表的背景、分片键的设计目标和建议,以及基因法的具体应用和优缺点。通过系统化的梳理,帮助读者提升架构、设计和开发水平,顺利通过面试。
美团面试:百亿级分片,如何设计基因算法?
|
1月前
|
算法
数据结构与算法二:栈、前缀、中缀、后缀表达式、中缀表达式转换为后缀表达式
这篇文章讲解了栈的基本概念及其应用,并详细介绍了中缀表达式转换为后缀表达式的算法和实现步骤。
43 3
|
1月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
66 2