# 前言
大家早上好,新的一节数据结构菜鸟课程又更新啦。
在这一节的内容中,带大家学习栈和队列的基础知识和常见题型。
栈和队列是我们面试过程中的常客,而且很多时候栈可以为我们很巧妙的解决一些问题,我们接下来会看到这些场景,有些时候也真的想拍下大腿,这也行?
# 栈和队列的基础知识
-
栈 (Stack)
- Last In, First Out: 我们经常当顺口溜一样说出来的
后进先出,即最新压入栈的元素最先被弹出。想象一叠盘子,最先放进去的盘子在最底部,最后放的盘子在最上面,取盘子时总是先从顶部开始。
-
栈的基本操作以及时间复杂度
操作 动作 时间复杂度 压栈 将一个元素压入栈顶 O(1) 出栈 从栈顶移除并返回该元素 O(1) 查看栈顶元素 返回栈顶元素但不移除它 O(1) -
- Last In, First Out: 我们经常当顺口溜一样说出来的
-
双端队列 (Deque)
双端队列(Deque,发音为 "deck")是一种线性数据结构,全称为 Double-Ended Queue,它的特点是允许在队列的两端进行元素的插入和删除操作。Deque 结合了栈(Stack)和队列(Queue)的特点,既可以在前端插入和删除元素,也可以在后端插入和删除元素。
主要的应用场景有:
滑动窗口算法,回文检查,任务调度,浏览器历史记录。Deque操作以及时间复杂度操作 时间复杂度 队头插入 O(1) 队尾插入 O(1) 删除 O(1) 访问 O(1) -
优先队列 (Priority Queue)
一般的队列是以 “时间” 为顺序的(先进先出)
优先队列按照元素的 “优先级” 取出,它可以是自己定义的一个元素属性。
许多数据结构都可以用来实现优先队列,例如二叉堆,二叉平衡树等。
Priority Queue 的操作以及时间复杂度操作 时间复杂度 访问最值 O(1) 插入 O(logN) 取最值 O(logN)
# 实战环节
-
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true示例 2:
输入:s = "()[]{}"
输出:true示例 3:
输入:s = "(]"
输出:false提示: 1 <= s.length <= 104
s 仅由括号 '()[]{}' 组成解题思路:我们遍历给定的输入字符串。遇到一个左侧括号,在后续的遍历中,我们期待一个相同类型的右括号和其配对,类似于连连看。由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。
遇到一个右括号时,期待一个相同类型的左括号匹配。取出栈顶的左括号并判断它们是否是相同类型的括号,如果不是相同的类型的括号,或者栈中并没有元素了,我们可以判定输入字符串不满足题目要求,返回 false。
在遍历完成后,如果栈中没有左括号,我们完成匹配过程,返回 True,否则返回 False。
这里有一个边界条件我们需要注意,就是有效字符串的长度一定是一个偶数,这样才可以一一配对。如果我们一上来就发现字符串长度是个奇数,那我们可以直接返回 False,后面的遍历过程都不用做啦。
示例代码:class Solution { public boolean isValid(String s) { Stack<Character> stack = new Stack<>(); for(char c : s.toCharArray()){ if(c == '(') stack.push(')'); else if(c == '[') stack.push(']'); else if(c == '{') stack.push('}'); else if( stack.isEmpty() || stack.pop() != c) return false; } return stack.isEmpty(); } } -
设计一个支持 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 次
解题思路:这个题目我们创建一个
min来存储对应当前栈的最小值,和一个栈来存储入栈元素。接下来我们分场景讨论如何维护栈和最小值。- 入栈: 比较入栈值和最小值关系,如果当前值比最小值
min要小的话,我们先入栈最小值,更新min,最后入栈该元素。 - 出栈:如果出栈的值等于最小值的话,说明出栈的元素的下一个元素是最小值,我们继续出栈元素,维护栈元素。
- 获取栈顶元素:直接查看栈的顶部元素。
- 获取最小值:直接查看
min。
示例代码:class MinStack { private int min; Deque<Integer> stack; public MinStack() { min = Integer.MAX_VALUE; stack = new LinkedList<>(); } public void push(int val) { if(val <= min) { stack.push(min); min = val; } stack.push(val); } public void pop() { if(stack.pop() == min) { min = stack.pop(); } } public int top() { return stack.peek(); } public int getMin() { return min; } } -
给你一个字符串数组 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 + * 也可以依据次序计算出正确结果。 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
解题思路:
逆波兰表达式严格遵循「从左到右」的运算规则。我们在计算逆波兰表达式时,使用一个栈来存储操作的数字,从左到右遍历逆波兰表达式,使用下面的规则来处理:-
如果遇到的是一个数字,那么我们将数字入栈。
-
如果遇到了运算符,则将栈顶的两个数字出栈。这里我们需要注意的是:先出栈的是右操作数,后出栈的是左操作数。接下来使用当前运算符对两个操作数进行运算,将运算得到的结果入栈,用来做接下来的运算。
-
当我们遍历完成后,栈内只有一个元素,该元素就是我们所要求的逆波兰表达式的结果。
示例代码:class Solution { public int evalRPN(String[] tokens) { Stack<Integer> stack = new Stack<>(); for(String token: tokens) { if(token.equals("+")) { int num1 = stack.pop(); int num2 = stack.pop(); stack.push(num2 + num1); } else if(token.equals("-")) { int num1 = stack.pop(); int num2 = stack.pop(); stack.push(num2 - num1); } else if(token.equals("*")) { int num1 = stack.pop(); int num2 = stack.pop(); stack.push(num2 * num1); } else if(token.equals("/")) { int num1 = stack.pop(); int num2 = stack.pop(); stack.push(num2 / num1); } else { stack.push(Integer.parseInt(token)); } } int result = stack.pop(); return result; } } -
给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval() 。
示例 1:
输入:s = "1 + 1"
输出:2示例 2:
输入:s = " 2-1 + 2 "
输出:3示例 3:
输入:s = "(1+(4+5+2)-3)+(6+8)"
输出:23提示:
- 1 <= s.length <= 3 * 105
- s 由数字、'+'、'-'、'('、')'、和 ' ' 组成
- s 表示一个有效的表达式
- '+' 不能用作一元运算(例如, "+1" 和 "+(2 + 3)" 无效)
- '-' 可以用作一元运算(即 "-1" 和 "-(2 + 3)" 是有效的)
- 输入中不存在两个连续的操作符
- 每个数字和运行的计算将适合于一个有符号的 32位 整数
解题思路:字符串只有加号和减号两种运算符。因此,在展开表达式的过程中,数字本身不会发生变化,只有数字前面的符号会发生变化。
我们使用取值为 -1 或者 +1 的整数
sign表示当前的符号。维护一个 stack,栈顶元素记录了当前位置所处的每个括号所形成的符号。遇到左括号
(时候,将当前 sign 压栈,遇到右括号)的时候,弹出一个元素。示例代码:class Solution { public int calculate(String s) { int result = 0; int sign = 1; int num = 0; int len = s.length(); Stack<Integer> stack = new Stack<>(); stack.push(sign); for(int i = 0; i < len; i++) { char curChar = s.charAt(i); if(curChar >= '0' && curChar <= '9') { num = num * 10 + curChar - '0'; } else if(curChar == '+' || curChar == '-') { result += sign * num; sign = stack.peek() * (curChar == '+' ? 1 : -1); num = 0; } else if(curChar == '(') { stack.push(sign); } else if(curChar == ')') { stack.pop(); } } result += sign * num; return result; } } -
给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
整数除法仅保留整数部分。
你可以假设给定的表达式总是有效的。所有中间结果将在 [-231, 231 - 1] 的范围内。
注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval() 。
示例 1:
输入:s = "3+2*2"
输出:7示例 2:
输入:s = " 3/2 "
输出:1示例 3:
输入:s = " 3+5 / 2 "
输出:5提示:
- 1 <= s.length <= 3 * 105
- s 由整数和算符 ('+', '-', '*', '/') 组成,中间由一些空格隔开
- s 表示一个 有效表达式
- 表达式中的所有整数都是非负整数,且在范围 [0, 231 - 1] 内
- 题目数据保证答案是一个 32-bit 整数
解题思路:先完成乘除运算,将乘除运算结果放回到原表达式的相应位置,则整个表达式的值,就等于一系列整数加减后的值。
我们创建一个栈,来保存这些整数的值。加减号后的数字,直接入栈。乘除后的数字,直接与栈顶元素计算,并替换成栈顶元素为计算后的结果。
使用
operation来记录之前的运算符。遍历到数字末尾时,根据operation来决定计算方式:- 加号:将数字压栈
- 减号:将数字的相反数压栈
- 乘除号:计算数字与栈顶元素,将栈顶元素替换为计算结果
最后,我们将栈中的元素累加起来,就是最后表达式计算的结果。
示例代码:class Solution { public int calculate(String s) { if(s == null || s.length() == 0) return 0; int currentNumber = 0; char operation = '+'; Stack<Integer> stack = new Stack<>(); int len = s.length(); for(int i = 0; i < len; i++) { char currentChar = s.charAt(i); if(Character.isDigit(currentChar)) { currentNumber = currentNumber * 10 + (currentChar - '0'); } if(!Character.isDigit(currentChar) && currentChar != ' ' || i == len - 1) { if(operation == '+') { stack.push(currentNumber); } else if(operation == '-') { stack.push(-currentNumber); } else if(operation == '*') { stack.push(stack.pop() * currentNumber); } else if(operation == '/') { stack.push(stack.pop() / currentNumber); } operation = currentChar; currentNumber = 0; } } int result = 0; while(!stack.isEmpty()) { result += stack.pop(); } return result; } }